##// END OF EJS Templates
Replaces find(:first/:all) calls....
Jean-Philippe Lang -
r10704:ea296a109a86
parent child
Show More
@@ -1,84 +1,83
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AdminController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :projects, :only => :projects
21 21 menu_item :plugins, :only => :plugins
22 22 menu_item :info, :only => :info
23 23
24 24 before_filter :require_admin
25 25 helper :sort
26 26 include SortHelper
27 27
28 28 def index
29 29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 30 end
31 31
32 32 def projects
33 33 @status = params[:status] || 1
34 34
35 scope = Project.status(@status)
35 scope = Project.status(@status).order('lft')
36 36 scope = scope.like(params[:name]) if params[:name].present?
37
38 @projects = scope.all(:order => 'lft')
37 @projects = scope.all
39 38
40 39 render :action => "projects", :layout => false if request.xhr?
41 40 end
42 41
43 42 def plugins
44 43 @plugins = Redmine::Plugin.all
45 44 end
46 45
47 46 # Loads the default configuration
48 47 # (roles, trackers, statuses, workflow, enumerations)
49 48 def default_configuration
50 49 if request.post?
51 50 begin
52 51 Redmine::DefaultData::Loader::load(params[:lang])
53 52 flash[:notice] = l(:notice_default_data_loaded)
54 53 rescue Exception => e
55 54 flash[:error] = l(:error_can_t_load_default_data, e.message)
56 55 end
57 56 end
58 57 redirect_to :action => 'index'
59 58 end
60 59
61 60 def test_email
62 61 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
63 62 # Force ActionMailer to raise delivery errors so we can catch it
64 63 ActionMailer::Base.raise_delivery_errors = true
65 64 begin
66 65 @test = Mailer.test_email(User.current).deliver
67 66 flash[:notice] = l(:notice_email_sent, User.current.mail)
68 67 rescue Exception => e
69 68 flash[:error] = l(:notice_email_error, e.message)
70 69 end
71 70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
72 71 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
73 72 end
74 73
75 74 def info
76 75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
77 76 @checklist = [
78 77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
79 78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
80 79 [:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
81 80 [:text_rmagick_available, Object.const_defined?(:Magick)]
82 81 ]
83 82 end
84 83 end
@@ -1,105 +1,110
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class BoardsController < ApplicationController
19 19 default_search_scope :messages
20 20 before_filter :find_project_by_project_id, :find_board_if_available, :authorize
21 21 accept_rss_auth :index, :show
22 22
23 23 helper :sort
24 24 include SortHelper
25 25 helper :watchers
26 26
27 27 def index
28 28 @boards = @project.boards.includes(:last_message => :author).all
29 29 # show the board if there is only one
30 30 if @boards.size == 1
31 31 @board = @boards.first
32 32 show
33 33 end
34 34 end
35 35
36 36 def show
37 37 respond_to do |format|
38 38 format.html {
39 39 sort_init 'updated_on', 'desc'
40 40 sort_update 'created_on' => "#{Message.table_name}.created_on",
41 41 'replies' => "#{Message.table_name}.replies_count",
42 42 'updated_on' => "#{Message.table_name}.updated_on"
43 43
44 44 @topic_count = @board.topics.count
45 45 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
46 @topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all(
47 :include => [:author, {:last_reply => :author}],
48 :limit => @topic_pages.items_per_page,
49 :offset => @topic_pages.current.offset)
46 @topics = @board.topics.
47 reorder("#{Message.table_name}.sticky DESC").
48 includes(:author, {:last_reply => :author}).
49 limit(@topic_pages.items_per_page).
50 offset(@topic_pages.current.offset).
51 order(sort_clause).
52 all
50 53 @message = Message.new(:board => @board)
51 54 render :action => 'show', :layout => !request.xhr?
52 55 }
53 56 format.atom {
54 @messages = @board.messages.find :all, :order => 'created_on DESC',
55 :include => [:author, :board],
56 :limit => Setting.feeds_limit.to_i
57 @messages = @board.messages.
58 reorder('created_on DESC').
59 includes(:author, :board).
60 limit(Setting.feeds_limit.to_i).
61 all
57 62 render_feed(@messages, :title => "#{@project}: #{@board}")
58 63 }
59 64 end
60 65 end
61 66
62 67 def new
63 68 @board = @project.boards.build
64 69 @board.safe_attributes = params[:board]
65 70 end
66 71
67 72 def create
68 73 @board = @project.boards.build
69 74 @board.safe_attributes = params[:board]
70 75 if @board.save
71 76 flash[:notice] = l(:notice_successful_create)
72 77 redirect_to_settings_in_projects
73 78 else
74 79 render :action => 'new'
75 80 end
76 81 end
77 82
78 83 def edit
79 84 end
80 85
81 86 def update
82 87 @board.safe_attributes = params[:board]
83 88 if @board.save
84 89 redirect_to_settings_in_projects
85 90 else
86 91 render :action => 'edit'
87 92 end
88 93 end
89 94
90 95 def destroy
91 96 @board.destroy
92 97 redirect_to_settings_in_projects
93 98 end
94 99
95 100 private
96 101 def redirect_to_settings_in_projects
97 102 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
98 103 end
99 104
100 105 def find_board_if_available
101 106 @board = @project.boards.find(params[:id]) if params[:id]
102 107 rescue ActiveRecord::RecordNotFound
103 108 render_404
104 109 end
105 110 end
@@ -1,94 +1,94
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class DocumentsController < ApplicationController
19 19 default_search_scope :documents
20 20 model_object Document
21 21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
22 22 before_filter :find_model_object, :except => [:index, :new, :create]
23 23 before_filter :find_project_from_association, :except => [:index, :new, :create]
24 24 before_filter :authorize
25 25
26 26 helper :attachments
27 27
28 28 def index
29 29 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
30 documents = @project.documents.find :all, :include => [:attachments, :category]
30 documents = @project.documents.includes(:attachments, :category).all
31 31 case @sort_by
32 32 when 'date'
33 33 @grouped = documents.group_by {|d| d.updated_on.to_date }
34 34 when 'title'
35 35 @grouped = documents.group_by {|d| d.title.first.upcase}
36 36 when 'author'
37 37 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
38 38 else
39 39 @grouped = documents.group_by(&:category)
40 40 end
41 41 @document = @project.documents.build
42 42 render :layout => false if request.xhr?
43 43 end
44 44
45 45 def show
46 46 @attachments = @document.attachments.all
47 47 end
48 48
49 49 def new
50 50 @document = @project.documents.build
51 51 @document.safe_attributes = params[:document]
52 52 end
53 53
54 54 def create
55 55 @document = @project.documents.build
56 56 @document.safe_attributes = params[:document]
57 57 @document.save_attachments(params[:attachments])
58 58 if @document.save
59 59 render_attachment_warning_if_needed(@document)
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to :action => 'index', :project_id => @project
62 62 else
63 63 render :action => 'new'
64 64 end
65 65 end
66 66
67 67 def edit
68 68 end
69 69
70 70 def update
71 71 @document.safe_attributes = params[:document]
72 72 if request.put? and @document.save
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to :action => 'show', :id => @document
75 75 else
76 76 render :action => 'edit'
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @document.destroy if request.delete?
82 82 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
83 83 end
84 84
85 85 def add_attachment
86 86 attachments = Attachment.attach_files(@document, params[:attachments])
87 87 render_attachment_warning_if_needed(@document)
88 88
89 89 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
90 90 Mailer.attachments_added(attachments[:files]).deliver
91 91 end
92 92 redirect_to :action => 'show', :id => @document
93 93 end
94 94 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TrackersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @trackers = Tracker.sorted.all
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @tracker ||= Tracker.new(params[:tracker])
39 @trackers = Tracker.find :all, :order => 'position'
39 @trackers = Tracker.sorted.all
40 40 @projects = Project.all
41 41 end
42 42
43 43 def create
44 44 @tracker = Tracker.new(params[:tracker])
45 45 if request.post? and @tracker.save
46 46 # workflow copy
47 47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 48 @tracker.workflow_rules.copy(copy_from)
49 49 end
50 50 flash[:notice] = l(:notice_successful_create)
51 51 redirect_to :action => 'index'
52 52 return
53 53 end
54 54 new
55 55 render :action => 'new'
56 56 end
57 57
58 58 def edit
59 59 @tracker ||= Tracker.find(params[:id])
60 60 @projects = Project.all
61 61 end
62 62
63 63 def update
64 64 @tracker = Tracker.find(params[:id])
65 65 if request.put? and @tracker.update_attributes(params[:tracker])
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to :action => 'index'
68 68 return
69 69 end
70 70 edit
71 71 render :action => 'edit'
72 72 end
73 73
74 74 def destroy
75 75 @tracker = Tracker.find(params[:id])
76 76 unless @tracker.issues.empty?
77 77 flash[:error] = l(:error_can_not_delete_tracker)
78 78 else
79 79 @tracker.destroy
80 80 end
81 81 redirect_to :action => 'index'
82 82 end
83 83
84 84 def fields
85 85 if request.post? && params[:trackers]
86 86 params[:trackers].each do |tracker_id, tracker_params|
87 87 tracker = Tracker.find_by_id(tracker_id)
88 88 if tracker
89 89 tracker.core_fields = tracker_params[:core_fields]
90 90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
91 91 tracker.save
92 92 end
93 93 end
94 94 flash[:notice] = l(:notice_successful_update)
95 95 redirect_to :action => 'fields'
96 96 return
97 97 end
98 98 @trackers = Tracker.sorted.all
99 99 @custom_fields = IssueCustomField.all.sort
100 100 end
101 101 end
@@ -1,214 +1,211
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
23 23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29
30 30 def index
31 31 sort_init 'login', 'asc'
32 32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33 33
34 34 case params[:format]
35 35 when 'xml', 'json'
36 36 @offset, @limit = api_offset_and_limit
37 37 else
38 38 @limit = per_page_option
39 39 end
40 40
41 41 @status = params[:status] || 1
42 42
43 43 scope = User.logged.status(@status)
44 44 scope = scope.like(params[:name]) if params[:name].present?
45 45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
46 46
47 47 @user_count = scope.count
48 48 @user_pages = Paginator.new self, @user_count, @limit, params['page']
49 49 @offset ||= @user_pages.current.offset
50 @users = scope.find :all,
51 :order => sort_clause,
52 :limit => @limit,
53 :offset => @offset
50 @users = scope.order(sort_clause).limit(@limit).offset(@offset).all
54 51
55 52 respond_to do |format|
56 53 format.html {
57 54 @groups = Group.all.sort
58 55 render :layout => !request.xhr?
59 56 }
60 57 format.api
61 58 end
62 59 end
63 60
64 61 def show
65 62 # show projects based on current user visibility
66 63 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
67 64
68 65 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
69 66 @events_by_day = events.group_by(&:event_date)
70 67
71 68 unless User.current.admin?
72 69 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
73 70 render_404
74 71 return
75 72 end
76 73 end
77 74
78 75 respond_to do |format|
79 76 format.html { render :layout => 'base' }
80 77 format.api
81 78 end
82 79 end
83 80
84 81 def new
85 82 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
86 83 @auth_sources = AuthSource.all
87 84 end
88 85
89 86 def create
90 87 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
91 88 @user.safe_attributes = params[:user]
92 89 @user.admin = params[:user][:admin] || false
93 90 @user.login = params[:user][:login]
94 91 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
95 92
96 93 if @user.save
97 94 @user.pref.attributes = params[:pref]
98 95 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
99 96 @user.pref.save
100 97 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
101 98
102 99 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
103 100
104 101 respond_to do |format|
105 102 format.html {
106 103 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
107 104 redirect_to(params[:continue] ?
108 105 {:controller => 'users', :action => 'new'} :
109 106 {:controller => 'users', :action => 'edit', :id => @user}
110 107 )
111 108 }
112 109 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
113 110 end
114 111 else
115 112 @auth_sources = AuthSource.all
116 113 # Clear password input
117 114 @user.password = @user.password_confirmation = nil
118 115
119 116 respond_to do |format|
120 117 format.html { render :action => 'new' }
121 118 format.api { render_validation_errors(@user) }
122 119 end
123 120 end
124 121 end
125 122
126 123 def edit
127 124 @auth_sources = AuthSource.all
128 125 @membership ||= Member.new
129 126 end
130 127
131 128 def update
132 129 @user.admin = params[:user][:admin] if params[:user][:admin]
133 130 @user.login = params[:user][:login] if params[:user][:login]
134 131 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
135 132 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
136 133 end
137 134 @user.safe_attributes = params[:user]
138 135 # Was the account actived ? (do it before User#save clears the change)
139 136 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
140 137 # TODO: Similar to My#account
141 138 @user.pref.attributes = params[:pref]
142 139 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
143 140
144 141 if @user.save
145 142 @user.pref.save
146 143 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
147 144
148 145 if was_activated
149 146 Mailer.account_activated(@user).deliver
150 147 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
151 148 Mailer.account_information(@user, params[:user][:password]).deliver
152 149 end
153 150
154 151 respond_to do |format|
155 152 format.html {
156 153 flash[:notice] = l(:notice_successful_update)
157 154 redirect_to_referer_or edit_user_path(@user)
158 155 }
159 156 format.api { render_api_ok }
160 157 end
161 158 else
162 159 @auth_sources = AuthSource.all
163 160 @membership ||= Member.new
164 161 # Clear password input
165 162 @user.password = @user.password_confirmation = nil
166 163
167 164 respond_to do |format|
168 165 format.html { render :action => :edit }
169 166 format.api { render_validation_errors(@user) }
170 167 end
171 168 end
172 169 end
173 170
174 171 def destroy
175 172 @user.destroy
176 173 respond_to do |format|
177 174 format.html { redirect_back_or_default(users_url) }
178 175 format.api { render_api_ok }
179 176 end
180 177 end
181 178
182 179 def edit_membership
183 180 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
184 181 @membership.save
185 182 respond_to do |format|
186 183 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
187 184 format.js
188 185 end
189 186 end
190 187
191 188 def destroy_membership
192 189 @membership = Member.find(params[:membership_id])
193 190 if @membership.deletable?
194 191 @membership.destroy
195 192 end
196 193 respond_to do |format|
197 194 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
198 195 format.js
199 196 end
200 197 end
201 198
202 199 private
203 200
204 201 def find_user
205 202 if params[:id] == 'current'
206 203 require_login || return
207 204 @user = User.current
208 205 else
209 206 @user = User.find(params[:id])
210 207 end
211 208 rescue ActiveRecord::RecordNotFound
212 209 render_404
213 210 end
214 211 end
@@ -1,354 +1,355
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'diff'
19 19
20 20 # The WikiController follows the Rails REST controller pattern but with
21 21 # a few differences
22 22 #
23 23 # * index - shows a list of WikiPages grouped by page or date
24 24 # * new - not used
25 25 # * create - not used
26 26 # * show - will also show the form for creating a new wiki page
27 27 # * edit - used to edit an existing or new page
28 28 # * update - used to save a wiki page update to the database, including new pages
29 29 # * destroy - normal
30 30 #
31 31 # Other member and collection methods are also used
32 32 #
33 33 # TODO: still being worked on
34 34 class WikiController < ApplicationController
35 35 default_search_scope :wiki_pages
36 36 before_filter :find_wiki, :authorize
37 37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39 39 accept_api_auth :index, :show, :update, :destroy
40 40
41 41 helper :attachments
42 42 include AttachmentsHelper
43 43 helper :watchers
44 44 include Redmine::Export::PDF
45 45
46 46 # List of pages, sorted alphabetically and by parent (hierarchy)
47 47 def index
48 48 load_pages_for_index
49 49
50 50 respond_to do |format|
51 51 format.html {
52 52 @pages_by_parent_id = @pages.group_by(&:parent_id)
53 53 }
54 54 format.api
55 55 end
56 56 end
57 57
58 58 # List of page, by last update
59 59 def date_index
60 60 load_pages_for_index
61 61 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
62 62 end
63 63
64 64 # display a page (in editing mode if it doesn't exist)
65 65 def show
66 66 if @page.new_record?
67 67 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
68 68 edit
69 69 render :action => 'edit'
70 70 else
71 71 render_404
72 72 end
73 73 return
74 74 end
75 75 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
76 76 deny_access
77 77 return
78 78 end
79 79 @content = @page.content_for_version(params[:version])
80 80 if User.current.allowed_to?(:export_wiki_pages, @project)
81 81 if params[:format] == 'pdf'
82 82 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
83 83 return
84 84 elsif params[:format] == 'html'
85 85 export = render_to_string :action => 'export', :layout => false
86 86 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
87 87 return
88 88 elsif params[:format] == 'txt'
89 89 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
90 90 return
91 91 end
92 92 end
93 93 @editable = editable?
94 94 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
95 95 @content.current_version? &&
96 96 Redmine::WikiFormatting.supports_section_edit?
97 97
98 98 respond_to do |format|
99 99 format.html
100 100 format.api
101 101 end
102 102 end
103 103
104 104 # edit an existing page or a new one
105 105 def edit
106 106 return render_403 unless editable?
107 107 if @page.new_record?
108 108 @page.content = WikiContent.new(:page => @page)
109 109 if params[:parent].present?
110 110 @page.parent = @page.wiki.find_page(params[:parent].to_s)
111 111 end
112 112 end
113 113
114 114 @content = @page.content_for_version(params[:version])
115 115 @content.text = initial_page_content(@page) if @content.text.blank?
116 116 # don't keep previous comment
117 117 @content.comments = nil
118 118
119 119 # To prevent StaleObjectError exception when reverting to a previous version
120 120 @content.version = @page.content.version
121 121
122 122 @text = @content.text
123 123 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
124 124 @section = params[:section].to_i
125 125 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
126 126 render_404 if @text.blank?
127 127 end
128 128 end
129 129
130 130 # Creates a new page or updates an existing one
131 131 def update
132 132 return render_403 unless editable?
133 133 was_new_page = @page.new_record?
134 134 @page.content = WikiContent.new(:page => @page) if @page.new_record?
135 135 @page.safe_attributes = params[:wiki_page]
136 136
137 137 @content = @page.content
138 138 content_params = params[:content]
139 139 if content_params.nil? && params[:wiki_page].is_a?(Hash)
140 140 content_params = params[:wiki_page].slice(:text, :comments, :version)
141 141 end
142 142 content_params ||= {}
143 143
144 144 @content.comments = content_params[:comments]
145 145 @text = content_params[:text]
146 146 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
147 147 @section = params[:section].to_i
148 148 @section_hash = params[:section_hash]
149 149 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
150 150 else
151 151 @content.version = content_params[:version] if content_params[:version]
152 152 @content.text = @text
153 153 end
154 154 @content.author = User.current
155 155
156 156 if @page.save_with_content
157 157 attachments = Attachment.attach_files(@page, params[:attachments])
158 158 render_attachment_warning_if_needed(@page)
159 159 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
160 160
161 161 respond_to do |format|
162 162 format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
163 163 format.api {
164 164 if was_new_page
165 165 render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
166 166 else
167 167 render_api_ok
168 168 end
169 169 }
170 170 end
171 171 else
172 172 respond_to do |format|
173 173 format.html { render :action => 'edit' }
174 174 format.api { render_validation_errors(@content) }
175 175 end
176 176 end
177 177
178 178 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
179 179 # Optimistic locking exception
180 180 respond_to do |format|
181 181 format.html {
182 182 flash.now[:error] = l(:notice_locking_conflict)
183 183 render :action => 'edit'
184 184 }
185 185 format.api { render_api_head :conflict }
186 186 end
187 187 rescue ActiveRecord::RecordNotSaved
188 188 respond_to do |format|
189 189 format.html { render :action => 'edit' }
190 190 format.api { render_validation_errors(@content) }
191 191 end
192 192 end
193 193
194 194 # rename a page
195 195 def rename
196 196 return render_403 unless editable?
197 197 @page.redirect_existing_links = true
198 198 # used to display the *original* title if some AR validation errors occur
199 199 @original_title = @page.pretty_title
200 200 if request.post? && @page.update_attributes(params[:wiki_page])
201 201 flash[:notice] = l(:notice_successful_update)
202 202 redirect_to :action => 'show', :project_id => @project, :id => @page.title
203 203 end
204 204 end
205 205
206 206 def protect
207 207 @page.update_attribute :protected, params[:protected]
208 208 redirect_to :action => 'show', :project_id => @project, :id => @page.title
209 209 end
210 210
211 211 # show page history
212 212 def history
213 213 @version_count = @page.content.versions.count
214 214 @version_pages = Paginator.new self, @version_count, per_page_option, params['page']
215 215 # don't load text
216 @versions = @page.content.versions.find :all,
217 :select => "id, author_id, comments, updated_on, version",
218 :order => 'version DESC',
219 :limit => @version_pages.items_per_page + 1,
220 :offset => @version_pages.current.offset
216 @versions = @page.content.versions.
217 select("id, author_id, comments, updated_on, version").
218 reorder('version DESC').
219 limit(@version_pages.items_per_page + 1).
220 offset(@version_pages.current.offset).
221 all
221 222
222 223 render :layout => false if request.xhr?
223 224 end
224 225
225 226 def diff
226 227 @diff = @page.diff(params[:version], params[:version_from])
227 228 render_404 unless @diff
228 229 end
229 230
230 231 def annotate
231 232 @annotate = @page.annotate(params[:version])
232 233 render_404 unless @annotate
233 234 end
234 235
235 236 # Removes a wiki page and its history
236 237 # Children can be either set as root pages, removed or reassigned to another parent page
237 238 def destroy
238 239 return render_403 unless editable?
239 240
240 241 @descendants_count = @page.descendants.size
241 242 if @descendants_count > 0
242 243 case params[:todo]
243 244 when 'nullify'
244 245 # Nothing to do
245 246 when 'destroy'
246 247 # Removes all its descendants
247 248 @page.descendants.each(&:destroy)
248 249 when 'reassign'
249 250 # Reassign children to another parent page
250 251 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
251 252 return unless reassign_to
252 253 @page.children.each do |child|
253 254 child.update_attribute(:parent, reassign_to)
254 255 end
255 256 else
256 257 @reassignable_to = @wiki.pages - @page.self_and_descendants
257 258 # display the destroy form if it's a user request
258 259 return unless api_request?
259 260 end
260 261 end
261 262 @page.destroy
262 263 respond_to do |format|
263 264 format.html { redirect_to :action => 'index', :project_id => @project }
264 265 format.api { render_api_ok }
265 266 end
266 267 end
267 268
268 269 def destroy_version
269 270 return render_403 unless editable?
270 271
271 272 @content = @page.content_for_version(params[:version])
272 273 @content.destroy
273 274 redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
274 275 end
275 276
276 277 # Export wiki to a single pdf or html file
277 278 def export
278 279 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
279 280 respond_to do |format|
280 281 format.html {
281 282 export = render_to_string :action => 'export_multiple', :layout => false
282 283 send_data(export, :type => 'text/html', :filename => "wiki.html")
283 284 }
284 285 format.pdf {
285 286 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
286 287 }
287 288 end
288 289 end
289 290
290 291 def preview
291 292 page = @wiki.find_page(params[:id])
292 293 # page is nil when previewing a new page
293 294 return render_403 unless page.nil? || editable?(page)
294 295 if page
295 296 @attachements = page.attachments
296 297 @previewed = page.content
297 298 end
298 299 @text = params[:content][:text]
299 300 render :partial => 'common/preview'
300 301 end
301 302
302 303 def add_attachment
303 304 return render_403 unless editable?
304 305 attachments = Attachment.attach_files(@page, params[:attachments])
305 306 render_attachment_warning_if_needed(@page)
306 307 redirect_to :action => 'show', :id => @page.title, :project_id => @project
307 308 end
308 309
309 310 private
310 311
311 312 def find_wiki
312 313 @project = Project.find(params[:project_id])
313 314 @wiki = @project.wiki
314 315 render_404 unless @wiki
315 316 rescue ActiveRecord::RecordNotFound
316 317 render_404
317 318 end
318 319
319 320 # Finds the requested page or a new page if it doesn't exist
320 321 def find_existing_or_new_page
321 322 @page = @wiki.find_or_new_page(params[:id])
322 323 if @wiki.page_found_with_redirect?
323 324 redirect_to params.update(:id => @page.title)
324 325 end
325 326 end
326 327
327 328 # Finds the requested page and returns a 404 error if it doesn't exist
328 329 def find_existing_page
329 330 @page = @wiki.find_page(params[:id])
330 331 if @page.nil?
331 332 render_404
332 333 return
333 334 end
334 335 if @wiki.page_found_with_redirect?
335 336 redirect_to params.update(:id => @page.title)
336 337 end
337 338 end
338 339
339 340 # Returns true if the current user is allowed to edit the page, otherwise false
340 341 def editable?(page = @page)
341 342 page.editable_by?(User.current)
342 343 end
343 344
344 345 # Returns the default content of a new wiki page
345 346 def initial_page_content(page)
346 347 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
347 348 extend helper unless self.instance_of?(helper)
348 349 helper.instance_method(:initial_page_content).bind(self).call(page)
349 350 end
350 351
351 352 def load_pages_for_index
352 353 @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
353 354 end
354 355 end
@@ -1,1079 +1,1086
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !@sortable.nil?
40 40 end
41 41
42 42 def sortable
43 43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 44 end
45 45
46 46 def value(issue)
47 47 issue.send name
48 48 end
49 49
50 50 def css_classes
51 51 name
52 52 end
53 53 end
54 54
55 55 class QueryCustomFieldColumn < QueryColumn
56 56
57 57 def initialize(custom_field)
58 58 self.name = "cf_#{custom_field.id}".to_sym
59 59 self.sortable = custom_field.order_statement || false
60 60 self.groupable = custom_field.group_statement || false
61 61 @cf = custom_field
62 62 end
63 63
64 64 def caption
65 65 @cf.name
66 66 end
67 67
68 68 def custom_field
69 69 @cf
70 70 end
71 71
72 72 def value(issue)
73 73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
75 75 end
76 76
77 77 def css_classes
78 78 @css_classes ||= "#{name} #{@cf.field_format}"
79 79 end
80 80 end
81 81
82 82 class Query < ActiveRecord::Base
83 83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 84 end
85 85
86 86 belongs_to :project
87 87 belongs_to :user
88 88 serialize :filters
89 89 serialize :column_names
90 90 serialize :sort_criteria, Array
91 91
92 92 attr_protected :project_id, :user_id
93 93
94 94 validates_presence_of :name
95 95 validates_length_of :name, :maximum => 255
96 96 validate :validate_query_filters
97 97
98 98 @@operators = { "=" => :label_equals,
99 99 "!" => :label_not_equals,
100 100 "o" => :label_open_issues,
101 101 "c" => :label_closed_issues,
102 102 "!*" => :label_none,
103 103 "*" => :label_any,
104 104 ">=" => :label_greater_or_equal,
105 105 "<=" => :label_less_or_equal,
106 106 "><" => :label_between,
107 107 "<t+" => :label_in_less_than,
108 108 ">t+" => :label_in_more_than,
109 109 "><t+"=> :label_in_the_next_days,
110 110 "t+" => :label_in,
111 111 "t" => :label_today,
112 112 "w" => :label_this_week,
113 113 ">t-" => :label_less_than_ago,
114 114 "<t-" => :label_more_than_ago,
115 115 "><t-"=> :label_in_the_past_days,
116 116 "t-" => :label_ago,
117 117 "~" => :label_contains,
118 118 "!~" => :label_not_contains,
119 119 "=p" => :label_any_issues_in_project,
120 120 "=!p" => :label_any_issues_not_in_project,
121 121 "!p" => :label_no_issues_in_project}
122 122
123 123 cattr_reader :operators
124 124
125 125 @@operators_by_filter_type = { :list => [ "=", "!" ],
126 126 :list_status => [ "o", "=", "!", "c", "*" ],
127 127 :list_optional => [ "=", "!", "!*", "*" ],
128 128 :list_subprojects => [ "*", "!*", "=" ],
129 129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
130 130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
131 131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
132 132 :text => [ "~", "!~", "!*", "*" ],
133 133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
134 134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
135 135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
136 136
137 137 cattr_reader :operators_by_filter_type
138 138
139 139 @@available_columns = [
140 140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
141 141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
142 142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
143 143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
144 144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
145 145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
146 146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
147 147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
148 148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
149 149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
150 150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
151 151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
152 152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
153 153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
154 154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
155 155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
156 156 QueryColumn.new(:relations, :caption => :label_related_issues)
157 157 ]
158 158 cattr_reader :available_columns
159 159
160 160 scope :visible, lambda {|*args|
161 161 user = args.shift || User.current
162 162 base = Project.allowed_to_condition(user, :view_issues, *args)
163 163 user_id = user.logged? ? user.id : 0
164 164 {
165 165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
166 166 :include => :project
167 167 }
168 168 }
169 169
170 170 def initialize(attributes=nil, *args)
171 171 super attributes
172 172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
173 173 @is_for_all = project.nil?
174 174 end
175 175
176 176 def validate_query_filters
177 177 filters.each_key do |field|
178 178 if values_for(field)
179 179 case type_for(field)
180 180 when :integer
181 181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 182 when :float
183 183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 184 when :date, :date_past
185 185 case operator_for(field)
186 186 when "=", ">=", "<=", "><"
187 187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 190 end
191 191 end
192 192 end
193 193
194 194 add_filter_error(field, :blank) unless
195 195 # filter requires one or more values
196 196 (values_for(field) and !values_for(field).first.blank?) or
197 197 # filter doesn't require any value
198 198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 199 end if filters
200 200 end
201 201
202 202 def add_filter_error(field, message)
203 203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
204 204 errors.add(:base, m)
205 205 end
206 206
207 207 # Returns true if the query is visible to +user+ or the current user.
208 208 def visible?(user=User.current)
209 209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
210 210 end
211 211
212 212 def editable_by?(user)
213 213 return false unless user
214 214 # Admin can edit them all and regular users can edit their private queries
215 215 return true if user.admin? || (!is_public && self.user_id == user.id)
216 216 # Members can not edit public queries that are for all project (only admin is allowed to)
217 217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
218 218 end
219 219
220 220 def trackers
221 221 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
222 222 end
223 223
224 224 # Returns a hash of localized labels for all filter operators
225 225 def self.operators_labels
226 226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
227 227 end
228 228
229 229 def available_filters
230 230 return @available_filters if @available_filters
231 231 @available_filters = {
232 232 "status_id" => {
233 233 :type => :list_status, :order => 0,
234 234 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
235 235 },
236 236 "tracker_id" => {
237 237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
238 238 },
239 239 "priority_id" => {
240 240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
241 241 },
242 242 "subject" => { :type => :text, :order => 8 },
243 243 "created_on" => { :type => :date_past, :order => 9 },
244 244 "updated_on" => { :type => :date_past, :order => 10 },
245 245 "start_date" => { :type => :date, :order => 11 },
246 246 "due_date" => { :type => :date, :order => 12 },
247 247 "estimated_hours" => { :type => :float, :order => 13 },
248 248 "done_ratio" => { :type => :integer, :order => 14 }
249 249 }
250 250 IssueRelation::TYPES.each do |relation_type, options|
251 251 @available_filters[relation_type] = {
252 252 :type => :relation, :order => @available_filters.size + 100,
253 253 :label => options[:name]
254 254 }
255 255 end
256 256 principals = []
257 257 if project
258 258 principals += project.principals.sort
259 259 unless project.leaf?
260 260 subprojects = project.descendants.visible.all
261 261 if subprojects.any?
262 262 @available_filters["subproject_id"] = {
263 263 :type => :list_subprojects, :order => 13,
264 264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
265 265 }
266 266 principals += Principal.member_of(subprojects)
267 267 end
268 268 end
269 269 else
270 270 if all_projects.any?
271 271 # members of visible projects
272 272 principals += Principal.member_of(all_projects)
273 273 # project filter
274 274 project_values = []
275 275 if User.current.logged? && User.current.memberships.any?
276 276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
277 277 end
278 278 project_values += all_projects_values
279 279 @available_filters["project_id"] = {
280 280 :type => :list, :order => 1, :values => project_values
281 281 } unless project_values.empty?
282 282 end
283 283 end
284 284 principals.uniq!
285 285 principals.sort!
286 286 users = principals.select {|p| p.is_a?(User)}
287 287
288 288 assigned_to_values = []
289 289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
290 290 assigned_to_values += (Setting.issue_group_assignment? ?
291 291 principals : users).collect{|s| [s.name, s.id.to_s] }
292 292 @available_filters["assigned_to_id"] = {
293 293 :type => :list_optional, :order => 4, :values => assigned_to_values
294 294 } unless assigned_to_values.empty?
295 295
296 296 author_values = []
297 297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
298 298 author_values += users.collect{|s| [s.name, s.id.to_s] }
299 299 @available_filters["author_id"] = {
300 300 :type => :list, :order => 5, :values => author_values
301 301 } unless author_values.empty?
302 302
303 303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
304 304 @available_filters["member_of_group"] = {
305 305 :type => :list_optional, :order => 6, :values => group_values
306 306 } unless group_values.empty?
307 307
308 308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
309 309 @available_filters["assigned_to_role"] = {
310 310 :type => :list_optional, :order => 7, :values => role_values
311 311 } unless role_values.empty?
312 312
313 313 if User.current.logged?
314 314 @available_filters["watcher_id"] = {
315 315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
316 316 }
317 317 end
318 318
319 319 if project
320 320 # project specific filters
321 321 categories = project.issue_categories.all
322 322 unless categories.empty?
323 323 @available_filters["category_id"] = {
324 324 :type => :list_optional, :order => 6,
325 325 :values => categories.collect{|s| [s.name, s.id.to_s] }
326 326 }
327 327 end
328 328 versions = project.shared_versions.all
329 329 unless versions.empty?
330 330 @available_filters["fixed_version_id"] = {
331 331 :type => :list_optional, :order => 7,
332 332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
333 333 }
334 334 end
335 335 add_custom_fields_filters(project.all_issue_custom_fields)
336 336 else
337 337 # global filters for cross project issue list
338 338 system_shared_versions = Version.visible.find_all_by_sharing('system')
339 339 unless system_shared_versions.empty?
340 340 @available_filters["fixed_version_id"] = {
341 341 :type => :list_optional, :order => 7,
342 342 :values => system_shared_versions.sort.collect{|s|
343 343 ["#{s.project.name} - #{s.name}", s.id.to_s]
344 344 }
345 345 }
346 346 end
347 347 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
348 348 end
349 349 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
350 350 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
351 351 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
352 352 @available_filters["is_private"] = {
353 353 :type => :list, :order => 16,
354 354 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
355 355 }
356 356 end
357 357 Tracker.disabled_core_fields(trackers).each {|field|
358 358 @available_filters.delete field
359 359 }
360 360 @available_filters.each do |field, options|
361 361 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
362 362 end
363 363 @available_filters
364 364 end
365 365
366 366 # Returns a representation of the available filters for JSON serialization
367 367 def available_filters_as_json
368 368 json = {}
369 369 available_filters.each do |field, options|
370 370 json[field] = options.slice(:type, :name, :values).stringify_keys
371 371 end
372 372 json
373 373 end
374 374
375 375 def all_projects
376 376 @all_projects ||= Project.visible.all
377 377 end
378 378
379 379 def all_projects_values
380 380 return @all_projects_values if @all_projects_values
381 381
382 382 values = []
383 383 Project.project_tree(all_projects) do |p, level|
384 384 prefix = (level > 0 ? ('--' * level + ' ') : '')
385 385 values << ["#{prefix}#{p.name}", p.id.to_s]
386 386 end
387 387 @all_projects_values = values
388 388 end
389 389
390 390 def add_filter(field, operator, values)
391 391 # values must be an array
392 392 return unless values.nil? || values.is_a?(Array)
393 393 # check if field is defined as an available filter
394 394 if available_filters.has_key? field
395 395 filter_options = available_filters[field]
396 396 # check if operator is allowed for that filter
397 397 #if @@operators_by_filter_type[filter_options[:type]].include? operator
398 398 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
399 399 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
400 400 #end
401 401 filters[field] = {:operator => operator, :values => (values || [''])}
402 402 end
403 403 end
404 404
405 405 def add_short_filter(field, expression)
406 406 return unless expression && available_filters.has_key?(field)
407 407 field_type = available_filters[field][:type]
408 408 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
409 409 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
410 410 add_filter field, operator, $1.present? ? $1.split('|') : ['']
411 411 end || add_filter(field, '=', expression.split('|'))
412 412 end
413 413
414 414 # Add multiple filters using +add_filter+
415 415 def add_filters(fields, operators, values)
416 416 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
417 417 fields.each do |field|
418 418 add_filter(field, operators[field], values && values[field])
419 419 end
420 420 end
421 421 end
422 422
423 423 def has_filter?(field)
424 424 filters and filters[field]
425 425 end
426 426
427 427 def type_for(field)
428 428 available_filters[field][:type] if available_filters.has_key?(field)
429 429 end
430 430
431 431 def operator_for(field)
432 432 has_filter?(field) ? filters[field][:operator] : nil
433 433 end
434 434
435 435 def values_for(field)
436 436 has_filter?(field) ? filters[field][:values] : nil
437 437 end
438 438
439 439 def value_for(field, index=0)
440 440 (values_for(field) || [])[index]
441 441 end
442 442
443 443 def label_for(field)
444 444 label = available_filters[field][:name] if available_filters.has_key?(field)
445 445 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
446 446 end
447 447
448 448 def available_columns
449 449 return @available_columns if @available_columns
450 450 @available_columns = ::Query.available_columns.dup
451 451 @available_columns += (project ?
452 452 project.all_issue_custom_fields :
453 453 IssueCustomField.all
454 454 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
455 455
456 456 if User.current.allowed_to?(:view_time_entries, project, :global => true)
457 457 index = nil
458 458 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
459 459 index = (index ? index + 1 : -1)
460 460 # insert the column after estimated_hours or at the end
461 461 @available_columns.insert index, QueryColumn.new(:spent_hours,
462 462 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
463 463 :default_order => 'desc',
464 464 :caption => :label_spent_time
465 465 )
466 466 end
467 467
468 468 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
469 469 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
470 470 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
471 471 end
472 472
473 473 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
474 474 @available_columns.reject! {|column|
475 475 disabled_fields.include?(column.name.to_s)
476 476 }
477 477
478 478 @available_columns
479 479 end
480 480
481 481 def self.available_columns=(v)
482 482 self.available_columns = (v)
483 483 end
484 484
485 485 def self.add_available_column(column)
486 486 self.available_columns << (column) if column.is_a?(QueryColumn)
487 487 end
488 488
489 489 # Returns an array of columns that can be used to group the results
490 490 def groupable_columns
491 491 available_columns.select {|c| c.groupable}
492 492 end
493 493
494 494 # Returns a Hash of columns and the key for sorting
495 495 def sortable_columns
496 496 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
497 497 h[column.name.to_s] = column.sortable
498 498 h
499 499 })
500 500 end
501 501
502 502 def columns
503 503 # preserve the column_names order
504 504 (has_default_columns? ? default_columns_names : column_names).collect do |name|
505 505 available_columns.find { |col| col.name == name }
506 506 end.compact
507 507 end
508 508
509 509 def default_columns_names
510 510 @default_columns_names ||= begin
511 511 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
512 512
513 513 project.present? ? default_columns : [:project] | default_columns
514 514 end
515 515 end
516 516
517 517 def column_names=(names)
518 518 if names
519 519 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
520 520 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
521 521 # Set column_names to nil if default columns
522 522 if names == default_columns_names
523 523 names = nil
524 524 end
525 525 end
526 526 write_attribute(:column_names, names)
527 527 end
528 528
529 529 def has_column?(column)
530 530 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
531 531 end
532 532
533 533 def has_default_columns?
534 534 column_names.nil? || column_names.empty?
535 535 end
536 536
537 537 def sort_criteria=(arg)
538 538 c = []
539 539 if arg.is_a?(Hash)
540 540 arg = arg.keys.sort.collect {|k| arg[k]}
541 541 end
542 542 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
543 543 write_attribute(:sort_criteria, c)
544 544 end
545 545
546 546 def sort_criteria
547 547 read_attribute(:sort_criteria) || []
548 548 end
549 549
550 550 def sort_criteria_key(arg)
551 551 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
552 552 end
553 553
554 554 def sort_criteria_order(arg)
555 555 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
556 556 end
557 557
558 558 def sort_criteria_order_for(key)
559 559 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
560 560 end
561 561
562 562 # Returns the SQL sort order that should be prepended for grouping
563 563 def group_by_sort_order
564 564 if grouped? && (column = group_by_column)
565 565 order = sort_criteria_order_for(column.name) || column.default_order
566 566 column.sortable.is_a?(Array) ?
567 567 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
568 568 "#{column.sortable} #{order}"
569 569 end
570 570 end
571 571
572 572 # Returns true if the query is a grouped query
573 573 def grouped?
574 574 !group_by_column.nil?
575 575 end
576 576
577 577 def group_by_column
578 578 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
579 579 end
580 580
581 581 def group_by_statement
582 582 group_by_column.try(:groupable)
583 583 end
584 584
585 585 def project_statement
586 586 project_clauses = []
587 587 if project && !project.descendants.active.empty?
588 588 ids = [project.id]
589 589 if has_filter?("subproject_id")
590 590 case operator_for("subproject_id")
591 591 when '='
592 592 # include the selected subprojects
593 593 ids += values_for("subproject_id").each(&:to_i)
594 594 when '!*'
595 595 # main project only
596 596 else
597 597 # all subprojects
598 598 ids += project.descendants.collect(&:id)
599 599 end
600 600 elsif Setting.display_subprojects_issues?
601 601 ids += project.descendants.collect(&:id)
602 602 end
603 603 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
604 604 elsif project
605 605 project_clauses << "#{Project.table_name}.id = %d" % project.id
606 606 end
607 607 project_clauses.any? ? project_clauses.join(' AND ') : nil
608 608 end
609 609
610 610 def statement
611 611 # filters clauses
612 612 filters_clauses = []
613 613 filters.each_key do |field|
614 614 next if field == "subproject_id"
615 615 v = values_for(field).clone
616 616 next unless v and !v.empty?
617 617 operator = operator_for(field)
618 618
619 619 # "me" value subsitution
620 620 if %w(assigned_to_id author_id watcher_id).include?(field)
621 621 if v.delete("me")
622 622 if User.current.logged?
623 623 v.push(User.current.id.to_s)
624 624 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
625 625 else
626 626 v.push("0")
627 627 end
628 628 end
629 629 end
630 630
631 631 if field == 'project_id'
632 632 if v.delete('mine')
633 633 v += User.current.memberships.map(&:project_id).map(&:to_s)
634 634 end
635 635 end
636 636
637 637 if field =~ /cf_(\d+)$/
638 638 # custom field
639 639 filters_clauses << sql_for_custom_field(field, operator, v, $1)
640 640 elsif respond_to?("sql_for_#{field}_field")
641 641 # specific statement
642 642 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
643 643 else
644 644 # regular field
645 645 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
646 646 end
647 647 end if filters and valid?
648 648
649 649 filters_clauses << project_statement
650 650 filters_clauses.reject!(&:blank?)
651 651
652 652 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
653 653 end
654 654
655 655 # Returns the issue count
656 656 def issue_count
657 657 Issue.visible.count(:include => [:status, :project], :conditions => statement)
658 658 rescue ::ActiveRecord::StatementInvalid => e
659 659 raise StatementInvalid.new(e.message)
660 660 end
661 661
662 662 # Returns the issue count by group or nil if query is not grouped
663 663 def issue_count_by_group
664 664 r = nil
665 665 if grouped?
666 666 begin
667 667 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
668 668 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
669 669 rescue ActiveRecord::RecordNotFound
670 670 r = {nil => issue_count}
671 671 end
672 672 c = group_by_column
673 673 if c.is_a?(QueryCustomFieldColumn)
674 674 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
675 675 end
676 676 end
677 677 r
678 678 rescue ::ActiveRecord::StatementInvalid => e
679 679 raise StatementInvalid.new(e.message)
680 680 end
681 681
682 682 # Returns the issues
683 683 # Valid options are :order, :offset, :limit, :include, :conditions
684 684 def issues(options={})
685 685 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
686 686 order_option = nil if order_option.blank?
687 687
688 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
689 :conditions => statement,
690 :order => order_option,
691 :joins => joins_for_order_statement(order_option),
692 :limit => options[:limit],
693 :offset => options[:offset]
688 issues = Issue.visible.where(options[:conditions]).all(
689 :include => ([:status, :project] + (options[:include] || [])).uniq,
690 :conditions => statement,
691 :order => order_option,
692 :joins => joins_for_order_statement(order_option),
693 :limit => options[:limit],
694 :offset => options[:offset]
695 )
694 696
695 697 if has_column?(:spent_hours)
696 698 Issue.load_visible_spent_hours(issues)
697 699 end
698 700 if has_column?(:relations)
699 701 Issue.load_visible_relations(issues)
700 702 end
701 703 issues
702 704 rescue ::ActiveRecord::StatementInvalid => e
703 705 raise StatementInvalid.new(e.message)
704 706 end
705 707
706 708 # Returns the issues ids
707 709 def issue_ids(options={})
708 710 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
709 711 order_option = nil if order_option.blank?
710 712
711 713 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
712 714 :conditions => statement,
713 715 :order => order_option,
714 716 :joins => joins_for_order_statement(order_option),
715 717 :limit => options[:limit],
716 718 :offset => options[:offset]).find_ids
717 719 rescue ::ActiveRecord::StatementInvalid => e
718 720 raise StatementInvalid.new(e.message)
719 721 end
720 722
721 723 # Returns the journals
722 724 # Valid options are :order, :offset, :limit
723 725 def journals(options={})
724 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
725 :conditions => statement,
726 :order => options[:order],
727 :limit => options[:limit],
728 :offset => options[:offset]
726 Journal.visible.all(
727 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
728 :conditions => statement,
729 :order => options[:order],
730 :limit => options[:limit],
731 :offset => options[:offset]
732 )
729 733 rescue ::ActiveRecord::StatementInvalid => e
730 734 raise StatementInvalid.new(e.message)
731 735 end
732 736
733 737 # Returns the versions
734 738 # Valid options are :conditions
735 739 def versions(options={})
736 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
740 Version.visible.where(options[:conditions]).all(
741 :include => :project,
742 :conditions => project_statement
743 )
737 744 rescue ::ActiveRecord::StatementInvalid => e
738 745 raise StatementInvalid.new(e.message)
739 746 end
740 747
741 748 def sql_for_watcher_id_field(field, operator, value)
742 749 db_table = Watcher.table_name
743 750 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
744 751 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
745 752 end
746 753
747 754 def sql_for_member_of_group_field(field, operator, value)
748 755 if operator == '*' # Any group
749 756 groups = Group.all
750 757 operator = '=' # Override the operator since we want to find by assigned_to
751 758 elsif operator == "!*"
752 759 groups = Group.all
753 760 operator = '!' # Override the operator since we want to find by assigned_to
754 761 else
755 762 groups = Group.find_all_by_id(value)
756 763 end
757 764 groups ||= []
758 765
759 766 members_of_groups = groups.inject([]) {|user_ids, group|
760 767 if group && group.user_ids.present?
761 768 user_ids << group.user_ids
762 769 end
763 770 user_ids.flatten.uniq.compact
764 771 }.sort.collect(&:to_s)
765 772
766 773 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
767 774 end
768 775
769 776 def sql_for_assigned_to_role_field(field, operator, value)
770 777 case operator
771 778 when "*", "!*" # Member / Not member
772 779 sw = operator == "!*" ? 'NOT' : ''
773 780 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
774 781 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
775 782 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
776 783 when "=", "!"
777 784 role_cond = value.any? ?
778 785 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
779 786 "1=0"
780 787
781 788 sw = operator == "!" ? 'NOT' : ''
782 789 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
783 790 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
784 791 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
785 792 end
786 793 end
787 794
788 795 def sql_for_is_private_field(field, operator, value)
789 796 op = (operator == "=" ? 'IN' : 'NOT IN')
790 797 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
791 798
792 799 "#{Issue.table_name}.is_private #{op} (#{va})"
793 800 end
794 801
795 802 def sql_for_relations(field, operator, value, options={})
796 803 relation_options = IssueRelation::TYPES[field]
797 804 return relation_options unless relation_options
798 805
799 806 relation_type = field
800 807 join_column, target_join_column = "issue_from_id", "issue_to_id"
801 808 if relation_options[:reverse] || options[:reverse]
802 809 relation_type = relation_options[:reverse] || relation_type
803 810 join_column, target_join_column = target_join_column, join_column
804 811 end
805 812
806 813 sql = case operator
807 814 when "*", "!*"
808 815 op = (operator == "*" ? 'IN' : 'NOT IN')
809 816 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
810 817 when "=", "!"
811 818 op = (operator == "=" ? 'IN' : 'NOT IN')
812 819 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
813 820 when "=p", "=!p", "!p"
814 821 op = (operator == "!p" ? 'NOT IN' : 'IN')
815 822 comp = (operator == "=!p" ? '<>' : '=')
816 823 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
817 824 end
818 825
819 826 if relation_options[:sym] == field && !options[:reverse]
820 827 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
821 828 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
822 829 else
823 830 sql
824 831 end
825 832 end
826 833
827 834 IssueRelation::TYPES.keys.each do |relation_type|
828 835 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
829 836 end
830 837
831 838 private
832 839
833 840 def sql_for_custom_field(field, operator, value, custom_field_id)
834 841 db_table = CustomValue.table_name
835 842 db_field = 'value'
836 843 filter = @available_filters[field]
837 844 return nil unless filter
838 845 if filter[:format] == 'user'
839 846 if value.delete('me')
840 847 value.push User.current.id.to_s
841 848 end
842 849 end
843 850 not_in = nil
844 851 if operator == '!'
845 852 # Makes ! operator work for custom fields with multiple values
846 853 operator = '='
847 854 not_in = 'NOT'
848 855 end
849 856 customized_key = "id"
850 857 customized_class = Issue
851 858 if field =~ /^(.+)\.cf_/
852 859 assoc = $1
853 860 customized_key = "#{assoc}_id"
854 861 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
855 862 raise "Unknown Issue association #{assoc}" unless customized_class
856 863 end
857 864 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
858 865 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
859 866 end
860 867
861 868 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
862 869 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
863 870 sql = ''
864 871 case operator
865 872 when "="
866 873 if value.any?
867 874 case type_for(field)
868 875 when :date, :date_past
869 876 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
870 877 when :integer
871 878 if is_custom_filter
872 879 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
873 880 else
874 881 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
875 882 end
876 883 when :float
877 884 if is_custom_filter
878 885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
879 886 else
880 887 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
881 888 end
882 889 else
883 890 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
884 891 end
885 892 else
886 893 # IN an empty set
887 894 sql = "1=0"
888 895 end
889 896 when "!"
890 897 if value.any?
891 898 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
892 899 else
893 900 # NOT IN an empty set
894 901 sql = "1=1"
895 902 end
896 903 when "!*"
897 904 sql = "#{db_table}.#{db_field} IS NULL"
898 905 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
899 906 when "*"
900 907 sql = "#{db_table}.#{db_field} IS NOT NULL"
901 908 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
902 909 when ">="
903 910 if [:date, :date_past].include?(type_for(field))
904 911 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
905 912 else
906 913 if is_custom_filter
907 914 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
908 915 else
909 916 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
910 917 end
911 918 end
912 919 when "<="
913 920 if [:date, :date_past].include?(type_for(field))
914 921 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
915 922 else
916 923 if is_custom_filter
917 924 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
918 925 else
919 926 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
920 927 end
921 928 end
922 929 when "><"
923 930 if [:date, :date_past].include?(type_for(field))
924 931 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
925 932 else
926 933 if is_custom_filter
927 934 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
928 935 else
929 936 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
930 937 end
931 938 end
932 939 when "o"
933 940 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
934 941 when "c"
935 942 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
936 943 when "><t-"
937 944 # between today - n days and today
938 945 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
939 946 when ">t-"
940 947 # >= today - n days
941 948 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
942 949 when "<t-"
943 950 # <= today - n days
944 951 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
945 952 when "t-"
946 953 # = n days in past
947 954 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
948 955 when "><t+"
949 956 # between today and today + n days
950 957 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
951 958 when ">t+"
952 959 # >= today + n days
953 960 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
954 961 when "<t+"
955 962 # <= today + n days
956 963 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
957 964 when "t+"
958 965 # = today + n days
959 966 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
960 967 when "t"
961 968 # = today
962 969 sql = relative_date_clause(db_table, db_field, 0, 0)
963 970 when "w"
964 971 # = this week
965 972 first_day_of_week = l(:general_first_day_of_week).to_i
966 973 day_of_week = Date.today.cwday
967 974 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
968 975 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
969 976 when "~"
970 977 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
971 978 when "!~"
972 979 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
973 980 else
974 981 raise "Unknown query operator #{operator}"
975 982 end
976 983
977 984 return sql
978 985 end
979 986
980 987 def add_custom_fields_filters(custom_fields, assoc=nil)
981 988 return unless custom_fields.present?
982 989 @available_filters ||= {}
983 990
984 991 custom_fields.select(&:is_filter?).each do |field|
985 992 case field.field_format
986 993 when "text"
987 994 options = { :type => :text, :order => 20 }
988 995 when "list"
989 996 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
990 997 when "date"
991 998 options = { :type => :date, :order => 20 }
992 999 when "bool"
993 1000 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
994 1001 when "int"
995 1002 options = { :type => :integer, :order => 20 }
996 1003 when "float"
997 1004 options = { :type => :float, :order => 20 }
998 1005 when "user", "version"
999 1006 next unless project
1000 1007 values = field.possible_values_options(project)
1001 1008 if User.current.logged? && field.field_format == 'user'
1002 1009 values.unshift ["<< #{l(:label_me)} >>", "me"]
1003 1010 end
1004 1011 options = { :type => :list_optional, :values => values, :order => 20}
1005 1012 else
1006 1013 options = { :type => :string, :order => 20 }
1007 1014 end
1008 1015 filter_id = "cf_#{field.id}"
1009 1016 filter_name = field.name
1010 1017 if assoc.present?
1011 1018 filter_id = "#{assoc}.#{filter_id}"
1012 1019 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1013 1020 end
1014 1021 @available_filters[filter_id] = options.merge({
1015 1022 :name => filter_name,
1016 1023 :format => field.field_format,
1017 1024 :field => field
1018 1025 })
1019 1026 end
1020 1027 end
1021 1028
1022 1029 def add_associations_custom_fields_filters(*associations)
1023 1030 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1024 1031 associations.each do |assoc|
1025 1032 association_klass = Issue.reflect_on_association(assoc).klass
1026 1033 fields_by_class.each do |field_class, fields|
1027 1034 if field_class.customized_class <= association_klass
1028 1035 add_custom_fields_filters(fields, assoc)
1029 1036 end
1030 1037 end
1031 1038 end
1032 1039 end
1033 1040
1034 1041 # Returns a SQL clause for a date or datetime field.
1035 1042 def date_clause(table, field, from, to)
1036 1043 s = []
1037 1044 if from
1038 1045 from_yesterday = from - 1
1039 1046 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1040 1047 if self.class.default_timezone == :utc
1041 1048 from_yesterday_time = from_yesterday_time.utc
1042 1049 end
1043 1050 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1044 1051 end
1045 1052 if to
1046 1053 to_time = Time.local(to.year, to.month, to.day)
1047 1054 if self.class.default_timezone == :utc
1048 1055 to_time = to_time.utc
1049 1056 end
1050 1057 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1051 1058 end
1052 1059 s.join(' AND ')
1053 1060 end
1054 1061
1055 1062 # Returns a SQL clause for a date or datetime field using relative dates.
1056 1063 def relative_date_clause(table, field, days_from, days_to)
1057 1064 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1058 1065 end
1059 1066
1060 1067 # Additional joins required for the given sort options
1061 1068 def joins_for_order_statement(order_options)
1062 1069 joins = []
1063 1070
1064 1071 if order_options
1065 1072 if order_options.include?('authors')
1066 1073 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1067 1074 end
1068 1075 order_options.scan(/cf_\d+/).uniq.each do |name|
1069 1076 column = available_columns.detect {|c| c.name.to_s == name}
1070 1077 join = column && column.custom_field.join_for_order_statement
1071 1078 if join
1072 1079 joins << join
1073 1080 end
1074 1081 end
1075 1082 end
1076 1083
1077 1084 joins.any? ? joins.join(' ') : nil
1078 1085 end
1079 1086 end
@@ -1,566 +1,568
1 1 # Copyright (c) 2005 Rick Olson
2 2 #
3 3 # Permission is hereby granted, free of charge, to any person obtaining
4 4 # a copy of this software and associated documentation files (the
5 5 # "Software"), to deal in the Software without restriction, including
6 6 # without limitation the rights to use, copy, modify, merge, publish,
7 7 # distribute, sublicense, and/or sell copies of the Software, and to
8 8 # permit persons to whom the Software is furnished to do so, subject to
9 9 # the following conditions:
10 10 #
11 11 # The above copyright notice and this permission notice shall be
12 12 # included in all copies or substantial portions of the Software.
13 13 #
14 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 21
22 22 module ActiveRecord #:nodoc:
23 23 module Acts #:nodoc:
24 24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26 26 # column is present as well.
27 27 #
28 28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
29 29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 30 #
31 31 # class Page < ActiveRecord::Base
32 32 # # assumes pages_versions table
33 33 # acts_as_versioned
34 34 # end
35 35 #
36 36 # Example:
37 37 #
38 38 # page = Page.create(:title => 'hello world!')
39 39 # page.version # => 1
40 40 #
41 41 # page.title = 'hello world'
42 42 # page.save
43 43 # page.version # => 2
44 44 # page.versions.size # => 2
45 45 #
46 46 # page.revert_to(1) # using version number
47 47 # page.title # => 'hello world!'
48 48 #
49 49 # page.revert_to(page.versions.last) # using versioned instance
50 50 # page.title # => 'hello world'
51 51 #
52 52 # page.versions.earliest # efficient query to find the first version
53 53 # page.versions.latest # efficient query to find the most recently created version
54 54 #
55 55 #
56 56 # Simple Queries to page between versions
57 57 #
58 58 # page.versions.before(version)
59 59 # page.versions.after(version)
60 60 #
61 61 # Access the previous/next versions from the versioned model itself
62 62 #
63 63 # version = page.versions.latest
64 64 # version.previous # go back one version
65 65 # version.next # go forward one version
66 66 #
67 67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68 68 module Versioned
69 69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
70 70 def self.included(base) # :nodoc:
71 71 base.extend ClassMethods
72 72 end
73 73
74 74 module ClassMethods
75 75 # == Configuration options
76 76 #
77 77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
78 78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
79 79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
80 80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
81 81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
82 82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
83 83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84 84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
85 85 # For finer control, pass either a Proc or modify Model#version_condition_met?
86 86 #
87 87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88 88 #
89 89 # or...
90 90 #
91 91 # class Auction
92 92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93 93 # !expired?
94 94 # end
95 95 # end
96 96 #
97 97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
98 98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
99 99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
100 100 #
101 101 # def name=(new_name)
102 102 # write_changed_attribute :name, new_name
103 103 # end
104 104 #
105 105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
106 106 # to create an anonymous mixin:
107 107 #
108 108 # class Auction
109 109 # acts_as_versioned do
110 110 # def started?
111 111 # !started_at.nil?
112 112 # end
113 113 # end
114 114 # end
115 115 #
116 116 # or...
117 117 #
118 118 # module AuctionExtension
119 119 # def started?
120 120 # !started_at.nil?
121 121 # end
122 122 # end
123 123 # class Auction
124 124 # acts_as_versioned :extend => AuctionExtension
125 125 # end
126 126 #
127 127 # Example code:
128 128 #
129 129 # @auction = Auction.find(1)
130 130 # @auction.started?
131 131 # @auction.versions.first.started?
132 132 #
133 133 # == Database Schema
134 134 #
135 135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
136 136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
137 137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
138 138 #
139 139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
140 140 # then that field is reflected in the versioned model as 'versioned_type' by default.
141 141 #
142 142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
143 143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
144 144 #
145 145 # class AddVersions < ActiveRecord::Migration
146 146 # def self.up
147 147 # # create_versioned_table takes the same options hash
148 148 # # that create_table does
149 149 # Post.create_versioned_table
150 150 # end
151 151 #
152 152 # def self.down
153 153 # Post.drop_versioned_table
154 154 # end
155 155 # end
156 156 #
157 157 # == Changing What Fields Are Versioned
158 158 #
159 159 # By default, acts_as_versioned will version all but these fields:
160 160 #
161 161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
162 162 #
163 163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
164 164 #
165 165 # class Post < ActiveRecord::Base
166 166 # acts_as_versioned
167 167 # self.non_versioned_columns << 'comments_count'
168 168 # end
169 169 #
170 170 def acts_as_versioned(options = {}, &extension)
171 171 # don't allow multiple calls
172 172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
173 173
174 174 send :include, ActiveRecord::Acts::Versioned::ActMethods
175 175
176 176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
177 177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
178 178 :version_association_options
179 179
180 180 # legacy
181 181 alias_method :non_versioned_fields, :non_versioned_columns
182 182 alias_method :non_versioned_fields=, :non_versioned_columns=
183 183
184 184 class << self
185 185 alias_method :non_versioned_fields, :non_versioned_columns
186 186 alias_method :non_versioned_fields=, :non_versioned_columns=
187 187 end
188 188
189 189 send :attr_accessor, :altered_attributes
190 190
191 191 self.versioned_class_name = options[:class_name] || "Version"
192 192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
193 193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
194 194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
195 195 self.version_column = options[:version_column] || 'version'
196 196 self.version_sequence_name = options[:sequence_name]
197 197 self.max_version_limit = options[:limit].to_i
198 198 self.version_condition = options[:if] || true
199 199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
200 200 self.version_association_options = {
201 201 :class_name => "#{self.to_s}::#{versioned_class_name}",
202 202 :foreign_key => versioned_foreign_key,
203 203 :dependent => :delete_all
204 204 }.merge(options[:association_options] || {})
205 205
206 206 if block_given?
207 207 extension_module_name = "#{versioned_class_name}Extension"
208 208 silence_warnings do
209 209 self.const_set(extension_module_name, Module.new(&extension))
210 210 end
211 211
212 212 options[:extend] = self.const_get(extension_module_name)
213 213 end
214 214
215 215 class_eval do
216 216 has_many :versions, version_association_options do
217 217 # finds earliest version of this record
218 218 def earliest
219 219 @earliest ||= order('version').first
220 220 end
221 221
222 222 # find latest version of this record
223 223 def latest
224 224 @latest ||= order('version desc').first
225 225 end
226 226 end
227 227 before_save :set_new_version
228 228 after_create :save_version_on_create
229 229 after_update :save_version
230 230 after_save :clear_old_versions
231 231 after_save :clear_altered_attributes
232 232
233 233 unless options[:if_changed].nil?
234 234 self.track_altered_attributes = true
235 235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
236 236 options[:if_changed].each do |attr_name|
237 237 define_method("#{attr_name}=") do |value|
238 238 write_changed_attribute attr_name, value
239 239 end
240 240 end
241 241 end
242 242
243 243 include options[:extend] if options[:extend].is_a?(Module)
244 244 end
245 245
246 246 # create the dynamic versioned model
247 247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
248 248 def self.reloadable? ; false ; end
249 249 # find first version before the given version
250 250 def self.before(version)
251 find :first, :order => 'version desc',
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
251 order('version desc').
252 where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
253 first
253 254 end
254 255
255 256 # find first version after the given version.
256 257 def self.after(version)
257 find :first, :order => 'version',
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
258 order('version').
259 where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
260 first
259 261 end
260 262
261 263 def previous
262 264 self.class.before(self)
263 265 end
264 266
265 267 def next
266 268 self.class.after(self)
267 269 end
268 270
269 271 def versions_count
270 272 page.version
271 273 end
272 274 end
273 275
274 276 versioned_class.cattr_accessor :original_class
275 277 versioned_class.original_class = self
276 278 versioned_class.table_name = versioned_table_name
277 279 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
278 280 :class_name => "::#{self.to_s}",
279 281 :foreign_key => versioned_foreign_key
280 282 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
281 283 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
282 284 end
283 285 end
284 286
285 287 module ActMethods
286 288 def self.included(base) # :nodoc:
287 289 base.extend ClassMethods
288 290 end
289 291
290 292 # Finds a specific version of this record
291 293 def find_version(version = nil)
292 294 self.class.find_version(id, version)
293 295 end
294 296
295 297 # Saves a version of the model if applicable
296 298 def save_version
297 299 save_version_on_create if save_version?
298 300 end
299 301
300 302 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
301 303 def save_version_on_create
302 304 rev = self.class.versioned_class.new
303 305 self.clone_versioned_model(self, rev)
304 306 rev.version = send(self.class.version_column)
305 307 rev.send("#{self.class.versioned_foreign_key}=", self.id)
306 308 rev.save
307 309 end
308 310
309 311 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
310 312 # Override this method to set your own criteria for clearing old versions.
311 313 def clear_old_versions
312 314 return if self.class.max_version_limit == 0
313 315 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
314 316 if excess_baggage > 0
315 317 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
316 318 self.class.versioned_class.connection.execute sql
317 319 end
318 320 end
319 321
320 322 def versions_count
321 323 version
322 324 end
323 325
324 326 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
325 327 def revert_to(version)
326 328 if version.is_a?(self.class.versioned_class)
327 329 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
328 330 else
329 331 return false unless version = versions.find_by_version(version)
330 332 end
331 333 self.clone_versioned_model(version, self)
332 334 self.send("#{self.class.version_column}=", version.version)
333 335 true
334 336 end
335 337
336 338 # Reverts a model to a given version and saves the model.
337 339 # Takes either a version number or an instance of the versioned model
338 340 def revert_to!(version)
339 341 revert_to(version) ? save_without_revision : false
340 342 end
341 343
342 344 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
343 345 def save_without_revision
344 346 save_without_revision!
345 347 true
346 348 rescue
347 349 false
348 350 end
349 351
350 352 def save_without_revision!
351 353 without_locking do
352 354 without_revision do
353 355 save!
354 356 end
355 357 end
356 358 end
357 359
358 360 # Returns an array of attribute keys that are versioned. See non_versioned_columns
359 361 def versioned_attributes
360 362 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
361 363 end
362 364
363 365 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
364 366 # If called with a single parameter, gets whether the parameter has changed.
365 367 def changed?(attr_name = nil)
366 368 attr_name.nil? ?
367 369 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
368 370 (altered_attributes && altered_attributes.include?(attr_name.to_s))
369 371 end
370 372
371 373 # keep old dirty? method
372 374 alias_method :dirty?, :changed?
373 375
374 376 # Clones a model. Used when saving a new version or reverting a model's version.
375 377 def clone_versioned_model(orig_model, new_model)
376 378 self.versioned_attributes.each do |key|
377 379 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
378 380 end
379 381
380 382 if self.class.columns_hash.include?(self.class.inheritance_column)
381 383 if orig_model.is_a?(self.class.versioned_class)
382 384 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
383 385 elsif new_model.is_a?(self.class.versioned_class)
384 386 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
385 387 end
386 388 end
387 389 end
388 390
389 391 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
390 392 def save_version?
391 393 version_condition_met? && changed?
392 394 end
393 395
394 396 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
395 397 # custom version condition checking.
396 398 def version_condition_met?
397 399 case
398 400 when version_condition.is_a?(Symbol)
399 401 send(version_condition)
400 402 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
401 403 version_condition.call(self)
402 404 else
403 405 version_condition
404 406 end
405 407 end
406 408
407 409 # Executes the block with the versioning callbacks disabled.
408 410 #
409 411 # @foo.without_revision do
410 412 # @foo.save
411 413 # end
412 414 #
413 415 def without_revision(&block)
414 416 self.class.without_revision(&block)
415 417 end
416 418
417 419 # Turns off optimistic locking for the duration of the block
418 420 #
419 421 # @foo.without_locking do
420 422 # @foo.save
421 423 # end
422 424 #
423 425 def without_locking(&block)
424 426 self.class.without_locking(&block)
425 427 end
426 428
427 429 def empty_callback() end #:nodoc:
428 430
429 431 protected
430 432 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
431 433 def set_new_version
432 434 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
433 435 end
434 436
435 437 # Gets the next available version for the current record, or 1 for a new record
436 438 def next_version
437 439 return 1 if new_record?
438 440 (versions.calculate(:max, :version) || 0) + 1
439 441 end
440 442
441 443 # clears current changed attributes. Called after save.
442 444 def clear_altered_attributes
443 445 self.altered_attributes = []
444 446 end
445 447
446 448 def write_changed_attribute(attr_name, attr_value)
447 449 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
448 450 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
449 451 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
450 452 write_attribute(attr_name, attr_value_for_db)
451 453 end
452 454
453 455 module ClassMethods
454 456 # Finds a specific version of a specific row of this model
455 457 def find_version(id, version = nil)
456 458 return find(id) unless version
457 459
458 460 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
459 461 options = { :conditions => conditions, :limit => 1 }
460 462
461 463 if result = find_versions(id, options).first
462 464 result
463 465 else
464 466 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
465 467 end
466 468 end
467 469
468 470 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
469 471 def find_versions(id, options = {})
470 versioned_class.find :all, {
472 versioned_class.all({
471 473 :conditions => ["#{versioned_foreign_key} = ?", id],
472 :order => 'version' }.merge(options)
474 :order => 'version' }.merge(options))
473 475 end
474 476
475 477 # Returns an array of columns that are versioned. See non_versioned_columns
476 478 def versioned_columns
477 479 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
478 480 end
479 481
480 482 # Returns an instance of the dynamic versioned model
481 483 def versioned_class
482 484 const_get versioned_class_name
483 485 end
484 486
485 487 # Rake migration task to create the versioned table using options passed to acts_as_versioned
486 488 def create_versioned_table(create_table_options = {})
487 489 # create version column in main table if it does not exist
488 490 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
489 491 self.connection.add_column table_name, :version, :integer
490 492 end
491 493
492 494 self.connection.create_table(versioned_table_name, create_table_options) do |t|
493 495 t.column versioned_foreign_key, :integer
494 496 t.column :version, :integer
495 497 end
496 498
497 499 updated_col = nil
498 500 self.versioned_columns.each do |col|
499 501 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
500 502 self.connection.add_column versioned_table_name, col.name, col.type,
501 503 :limit => col.limit,
502 504 :default => col.default,
503 505 :scale => col.scale,
504 506 :precision => col.precision
505 507 end
506 508
507 509 if type_col = self.columns_hash[inheritance_column]
508 510 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
509 511 :limit => type_col.limit,
510 512 :default => type_col.default,
511 513 :scale => type_col.scale,
512 514 :precision => type_col.precision
513 515 end
514 516
515 517 if updated_col.nil?
516 518 self.connection.add_column versioned_table_name, :updated_at, :timestamp
517 519 end
518 520 end
519 521
520 522 # Rake migration task to drop the versioned table
521 523 def drop_versioned_table
522 524 self.connection.drop_table versioned_table_name
523 525 end
524 526
525 527 # Executes the block with the versioning callbacks disabled.
526 528 #
527 529 # Foo.without_revision do
528 530 # @foo.save
529 531 # end
530 532 #
531 533 def without_revision(&block)
532 534 class_eval do
533 535 CALLBACKS.each do |attr_name|
534 536 alias_method "orig_#{attr_name}".to_sym, attr_name
535 537 alias_method attr_name, :empty_callback
536 538 end
537 539 end
538 540 block.call
539 541 ensure
540 542 class_eval do
541 543 CALLBACKS.each do |attr_name|
542 544 alias_method attr_name, "orig_#{attr_name}".to_sym
543 545 end
544 546 end
545 547 end
546 548
547 549 # Turns off optimistic locking for the duration of the block
548 550 #
549 551 # Foo.without_locking do
550 552 # @foo.save
551 553 # end
552 554 #
553 555 def without_locking(&block)
554 556 current = ActiveRecord::Base.lock_optimistically
555 557 ActiveRecord::Base.lock_optimistically = false if current
556 558 result = block.call
557 559 ActiveRecord::Base.lock_optimistically = true if current
558 560 result
559 561 end
560 562 end
561 563 end
562 564 end
563 565 end
564 566 end
565 567
566 568 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
@@ -1,511 +1,511
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 desc 'Mantis migration script'
19 19
20 20 require 'active_record'
21 21 require 'iconv'
22 22 require 'pp'
23 23
24 24 namespace :redmine do
25 25 task :migrate_from_mantis => :environment do
26 26
27 27 module MantisMigrate
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.where(:is_closed => true).first
34 34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 35 20 => feedback_status, # feedback
36 36 30 => DEFAULT_STATUS, # acknowledged
37 37 40 => DEFAULT_STATUS, # confirmed
38 38 50 => assigned_status, # assigned
39 39 80 => resolved_status, # resolved
40 40 90 => closed_status # closed
41 41 }
42 42
43 43 priorities = IssuePriority.all
44 44 DEFAULT_PRIORITY = priorities[2]
45 45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 46 20 => priorities[1], # low
47 47 30 => priorities[2], # normal
48 48 40 => priorities[3], # high
49 49 50 => priorities[4], # urgent
50 50 60 => priorities[5] # immediate
51 51 }
52 52
53 53 TRACKER_BUG = Tracker.find_by_position(1)
54 54 TRACKER_FEATURE = Tracker.find_by_position(2)
55 55
56 56 roles = Role.where(:builtin => 0).order('position ASC').all
57 57 manager_role = roles[0]
58 58 developer_role = roles[1]
59 59 DEFAULT_ROLE = roles.last
60 60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 61 25 => DEFAULT_ROLE, # reporter
62 62 40 => DEFAULT_ROLE, # updater
63 63 55 => developer_role, # developer
64 64 70 => manager_role, # manager
65 65 90 => manager_role # administrator
66 66 }
67 67
68 68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 69 1 => 'int', # Numeric
70 70 2 => 'int', # Float
71 71 3 => 'list', # Enumeration
72 72 4 => 'string', # Email
73 73 5 => 'bool', # Checkbox
74 74 6 => 'list', # List
75 75 7 => 'list', # Multiselection list
76 76 8 => 'date', # Date
77 77 }
78 78
79 79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 80 2 => IssueRelation::TYPE_RELATES, # parent of
81 81 3 => IssueRelation::TYPE_RELATES, # child of
82 82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 84 }
85 85
86 86 class MantisUser < ActiveRecord::Base
87 87 self.table_name = :mantis_user_table
88 88
89 89 def firstname
90 90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 91 @firstname
92 92 end
93 93
94 94 def lastname
95 95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 96 @lastname = '-' if @lastname.blank?
97 97 @lastname
98 98 end
99 99
100 100 def email
101 101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
102 102 !User.find_by_mail(read_attribute(:email))
103 103 @email = read_attribute(:email)
104 104 else
105 105 @email = "#{username}@foo.bar"
106 106 end
107 107 end
108 108
109 109 def username
110 110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
111 111 end
112 112 end
113 113
114 114 class MantisProject < ActiveRecord::Base
115 115 self.table_name = :mantis_project_table
116 116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
117 117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
118 118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
119 119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
120 120
121 121 def identifier
122 122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
123 123 end
124 124 end
125 125
126 126 class MantisVersion < ActiveRecord::Base
127 127 self.table_name = :mantis_project_version_table
128 128
129 129 def version
130 130 read_attribute(:version)[0..29]
131 131 end
132 132
133 133 def description
134 134 read_attribute(:description)[0..254]
135 135 end
136 136 end
137 137
138 138 class MantisCategory < ActiveRecord::Base
139 139 self.table_name = :mantis_project_category_table
140 140 end
141 141
142 142 class MantisProjectUser < ActiveRecord::Base
143 143 self.table_name = :mantis_project_user_list_table
144 144 end
145 145
146 146 class MantisBug < ActiveRecord::Base
147 147 self.table_name = :mantis_bug_table
148 148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
149 149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
150 150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
151 151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
152 152 end
153 153
154 154 class MantisBugText < ActiveRecord::Base
155 155 self.table_name = :mantis_bug_text_table
156 156
157 157 # Adds Mantis steps_to_reproduce and additional_information fields
158 158 # to description if any
159 159 def full_description
160 160 full_description = description
161 161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
162 162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
163 163 full_description
164 164 end
165 165 end
166 166
167 167 class MantisBugNote < ActiveRecord::Base
168 168 self.table_name = :mantis_bugnote_table
169 169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
170 170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
171 171 end
172 172
173 173 class MantisBugNoteText < ActiveRecord::Base
174 174 self.table_name = :mantis_bugnote_text_table
175 175 end
176 176
177 177 class MantisBugFile < ActiveRecord::Base
178 178 self.table_name = :mantis_bug_file_table
179 179
180 180 def size
181 181 filesize
182 182 end
183 183
184 184 def original_filename
185 185 MantisMigrate.encode(filename)
186 186 end
187 187
188 188 def content_type
189 189 file_type
190 190 end
191 191
192 192 def read(*args)
193 193 if @read_finished
194 194 nil
195 195 else
196 196 @read_finished = true
197 197 content
198 198 end
199 199 end
200 200 end
201 201
202 202 class MantisBugRelationship < ActiveRecord::Base
203 203 self.table_name = :mantis_bug_relationship_table
204 204 end
205 205
206 206 class MantisBugMonitor < ActiveRecord::Base
207 207 self.table_name = :mantis_bug_monitor_table
208 208 end
209 209
210 210 class MantisNews < ActiveRecord::Base
211 211 self.table_name = :mantis_news_table
212 212 end
213 213
214 214 class MantisCustomField < ActiveRecord::Base
215 215 self.table_name = :mantis_custom_field_table
216 216 set_inheritance_column :none
217 217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219 219
220 220 def format
221 221 read_attribute :type
222 222 end
223 223
224 224 def name
225 225 read_attribute(:name)[0..29]
226 226 end
227 227 end
228 228
229 229 class MantisCustomFieldProject < ActiveRecord::Base
230 230 self.table_name = :mantis_custom_field_project_table
231 231 end
232 232
233 233 class MantisCustomFieldString < ActiveRecord::Base
234 234 self.table_name = :mantis_custom_field_string_table
235 235 end
236 236
237 237 def self.migrate
238 238
239 239 # Users
240 240 print "Migrating users"
241 241 User.delete_all "login <> 'admin'"
242 242 users_map = {}
243 243 users_migrated = 0
244 244 MantisUser.all.each do |user|
245 245 u = User.new :firstname => encode(user.firstname),
246 246 :lastname => encode(user.lastname),
247 247 :mail => user.email,
248 248 :last_login_on => user.last_visit
249 249 u.login = user.username
250 250 u.password = 'mantis'
251 251 u.status = User::STATUS_LOCKED if user.enabled != 1
252 252 u.admin = true if user.access_level == 90
253 253 next unless u.save!
254 254 users_migrated += 1
255 255 users_map[user.id] = u.id
256 256 print '.'
257 257 end
258 258 puts
259 259
260 260 # Projects
261 261 print "Migrating projects"
262 262 Project.destroy_all
263 263 projects_map = {}
264 264 versions_map = {}
265 265 categories_map = {}
266 266 MantisProject.all.each do |project|
267 267 p = Project.new :name => encode(project.name),
268 268 :description => encode(project.description)
269 269 p.identifier = project.identifier
270 270 next unless p.save
271 271 projects_map[project.id] = p.id
272 272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
273 273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
274 274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
275 275 print '.'
276 276
277 277 # Project members
278 278 project.members.each do |member|
279 279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
280 280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
281 281 m.project = p
282 282 m.save
283 283 end
284 284
285 285 # Project versions
286 286 project.versions.each do |version|
287 287 v = Version.new :name => encode(version.version),
288 288 :description => encode(version.description),
289 289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
290 290 v.project = p
291 291 v.save
292 292 versions_map[version.id] = v.id
293 293 end
294 294
295 295 # Project categories
296 296 project.categories.each do |category|
297 297 g = IssueCategory.new :name => category.category[0,30]
298 298 g.project = p
299 299 g.save
300 300 categories_map[category.category] = g.id
301 301 end
302 302 end
303 303 puts
304 304
305 305 # Bugs
306 306 print "Migrating bugs"
307 307 Issue.destroy_all
308 308 issues_map = {}
309 309 keep_bug_ids = (Issue.count == 0)
310 310 MantisBug.find_each(:batch_size => 200) do |bug|
311 311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
312 312 i = Issue.new :project_id => projects_map[bug.project_id],
313 313 :subject => encode(bug.summary),
314 314 :description => encode(bug.bug_text.full_description),
315 315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
316 316 :created_on => bug.date_submitted,
317 317 :updated_on => bug.last_updated
318 318 i.author = User.find_by_id(users_map[bug.reporter_id])
319 319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
320 320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
321 321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
322 322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
323 323 i.id = bug.id if keep_bug_ids
324 324 next unless i.save
325 325 issues_map[bug.id] = i.id
326 326 print '.'
327 327 STDOUT.flush
328 328
329 329 # Assignee
330 330 # Redmine checks that the assignee is a project member
331 331 if (bug.handler_id && users_map[bug.handler_id])
332 332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 333 i.save(:validate => false)
334 334 end
335 335
336 336 # Bug notes
337 337 bug.bug_notes.each do |note|
338 338 next unless users_map[note.reporter_id]
339 339 n = Journal.new :notes => encode(note.bug_note_text.note),
340 340 :created_on => note.date_submitted
341 341 n.user = User.find_by_id(users_map[note.reporter_id])
342 342 n.journalized = i
343 343 n.save
344 344 end
345 345
346 346 # Bug files
347 347 bug.bug_files.each do |file|
348 348 a = Attachment.new :created_on => file.date_added
349 349 a.file = file
350 a.author = User.find :first
350 a.author = User.first
351 351 a.container = i
352 352 a.save
353 353 end
354 354
355 355 # Bug monitors
356 356 bug.bug_monitors.each do |monitor|
357 357 next unless users_map[monitor.user_id]
358 358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 359 end
360 360 end
361 361
362 362 # update issue id sequence if needed (postgresql)
363 363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 364 puts
365 365
366 366 # Bug relationships
367 367 print "Migrating bug relations"
368 368 MantisBugRelationship.all.each do |relation|
369 369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 373 pp r unless r.save
374 374 print '.'
375 375 STDOUT.flush
376 376 end
377 377 puts
378 378
379 379 # News
380 380 print "Migrating news"
381 381 News.destroy_all
382 382 MantisNews.where('project_id > 0').all.each do |news|
383 383 next unless projects_map[news.project_id]
384 384 n = News.new :project_id => projects_map[news.project_id],
385 385 :title => encode(news.headline[0..59]),
386 386 :description => encode(news.body),
387 387 :created_on => news.date_posted
388 388 n.author = User.find_by_id(users_map[news.poster_id])
389 389 n.save
390 390 print '.'
391 391 STDOUT.flush
392 392 end
393 393 puts
394 394
395 395 # Custom fields
396 396 print "Migrating custom fields"
397 397 IssueCustomField.destroy_all
398 398 MantisCustomField.all.each do |field|
399 399 f = IssueCustomField.new :name => field.name[0..29],
400 400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
401 401 :min_length => field.length_min,
402 402 :max_length => field.length_max,
403 403 :regexp => field.valid_regexp,
404 404 :possible_values => field.possible_values.split('|'),
405 405 :is_required => field.require_report?
406 406 next unless f.save
407 407 print '.'
408 408 STDOUT.flush
409 409 # Trackers association
410 410 f.trackers = Tracker.all
411 411
412 412 # Projects association
413 413 field.projects.each do |project|
414 414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
415 415 end
416 416
417 417 # Values
418 418 field.values.each do |value|
419 419 v = CustomValue.new :custom_field_id => f.id,
420 420 :value => value.value
421 421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
422 422 v.save
423 423 end unless f.new_record?
424 424 end
425 425 puts
426 426
427 427 puts
428 428 puts "Users: #{users_migrated}/#{MantisUser.count}"
429 429 puts "Projects: #{Project.count}/#{MantisProject.count}"
430 430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
431 431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
432 432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
433 433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
434 434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
435 435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
436 436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
437 437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
438 438 puts "News: #{News.count}/#{MantisNews.count}"
439 439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
440 440 end
441 441
442 442 def self.encoding(charset)
443 443 @ic = Iconv.new('UTF-8', charset)
444 444 rescue Iconv::InvalidEncoding
445 445 return false
446 446 end
447 447
448 448 def self.establish_connection(params)
449 449 constants.each do |const|
450 450 klass = const_get(const)
451 451 next unless klass.respond_to? 'establish_connection'
452 452 klass.establish_connection params
453 453 end
454 454 end
455 455
456 456 def self.encode(text)
457 457 @ic.iconv text
458 458 rescue
459 459 text
460 460 end
461 461 end
462 462
463 463 puts
464 464 if Redmine::DefaultData::Loader.no_data?
465 465 puts "Redmine configuration need to be loaded before importing data."
466 466 puts "Please, run this first:"
467 467 puts
468 468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 469 exit
470 470 end
471 471
472 472 puts "WARNING: Your Redmine data will be deleted during this process."
473 473 print "Are you sure you want to continue ? [y/N] "
474 474 STDOUT.flush
475 475 break unless STDIN.gets.match(/^y$/i)
476 476
477 477 # Default Mantis database settings
478 478 db_params = {:adapter => 'mysql2',
479 479 :database => 'bugtracker',
480 480 :host => 'localhost',
481 481 :username => 'root',
482 482 :password => '' }
483 483
484 484 puts
485 485 puts "Please enter settings for your Mantis database"
486 486 [:adapter, :host, :database, :username, :password].each do |param|
487 487 print "#{param} [#{db_params[param]}]: "
488 488 value = STDIN.gets.chomp!
489 489 db_params[param] = value unless value.blank?
490 490 end
491 491
492 492 while true
493 493 print "encoding [UTF-8]: "
494 494 STDOUT.flush
495 495 encoding = STDIN.gets.chomp!
496 496 encoding = 'UTF-8' if encoding.blank?
497 497 break if MantisMigrate.encoding encoding
498 498 puts "Invalid encoding!"
499 499 end
500 500 puts
501 501
502 502 # Make sure bugs can refer bugs in other projects
503 503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504 504
505 505 # Turn off email notifications
506 506 Setting.notified_events = []
507 507
508 508 MantisMigrate.establish_connection db_params
509 509 MantisMigrate.migrate
510 510 end
511 511 end
@@ -1,772 +1,772
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'active_record'
19 19 require 'iconv'
20 20 require 'pp'
21 21
22 22 namespace :redmine do
23 23 desc 'Trac migration script'
24 24 task :migrate_from_trac => :environment do
25 25
26 26 module TracMigrate
27 27 TICKET_MAP = []
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.where(:is_closed => true).first
34 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 35 'reopened' => feedback_status,
36 36 'assigned' => assigned_status,
37 37 'closed' => closed_status
38 38 }
39 39
40 40 priorities = IssuePriority.all
41 41 DEFAULT_PRIORITY = priorities[0]
42 42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 43 'low' => priorities[0],
44 44 'normal' => priorities[1],
45 45 'high' => priorities[2],
46 46 'highest' => priorities[3],
47 47 # ---
48 48 'trivial' => priorities[0],
49 49 'minor' => priorities[1],
50 50 'major' => priorities[2],
51 51 'critical' => priorities[3],
52 52 'blocker' => priorities[4]
53 53 }
54 54
55 55 TRACKER_BUG = Tracker.find_by_position(1)
56 56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 57 DEFAULT_TRACKER = TRACKER_BUG
58 58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 59 'enhancement' => TRACKER_FEATURE,
60 60 'task' => TRACKER_FEATURE,
61 61 'patch' =>TRACKER_FEATURE
62 62 }
63 63
64 64 roles = Role.where(:builtin => 0).order('position ASC').all
65 65 manager_role = roles[0]
66 66 developer_role = roles[1]
67 67 DEFAULT_ROLE = roles.last
68 68 ROLE_MAPPING = {'admin' => manager_role,
69 69 'developer' => developer_role
70 70 }
71 71
72 72 class ::Time
73 73 class << self
74 74 alias :real_now :now
75 75 def now
76 76 real_now - @fake_diff.to_i
77 77 end
78 78 def fake(time)
79 79 @fake_diff = real_now - time
80 80 res = yield
81 81 @fake_diff = 0
82 82 res
83 83 end
84 84 end
85 85 end
86 86
87 87 class TracComponent < ActiveRecord::Base
88 88 self.table_name = :component
89 89 end
90 90
91 91 class TracMilestone < ActiveRecord::Base
92 92 self.table_name = :milestone
93 93 # If this attribute is set a milestone has a defined target timepoint
94 94 def due
95 95 if read_attribute(:due) && read_attribute(:due) > 0
96 96 Time.at(read_attribute(:due)).to_date
97 97 else
98 98 nil
99 99 end
100 100 end
101 101 # This is the real timepoint at which the milestone has finished.
102 102 def completed
103 103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 104 Time.at(read_attribute(:completed)).to_date
105 105 else
106 106 nil
107 107 end
108 108 end
109 109
110 110 def description
111 111 # Attribute is named descr in Trac v0.8.x
112 112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 113 end
114 114 end
115 115
116 116 class TracTicketCustom < ActiveRecord::Base
117 117 self.table_name = :ticket_custom
118 118 end
119 119
120 120 class TracAttachment < ActiveRecord::Base
121 121 self.table_name = :attachment
122 122 set_inheritance_column :none
123 123
124 124 def time; Time.at(read_attribute(:time)) end
125 125
126 126 def original_filename
127 127 filename
128 128 end
129 129
130 130 def content_type
131 131 ''
132 132 end
133 133
134 134 def exist?
135 135 File.file? trac_fullpath
136 136 end
137 137
138 138 def open
139 139 File.open("#{trac_fullpath}", 'rb') {|f|
140 140 @file = f
141 141 yield self
142 142 }
143 143 end
144 144
145 145 def read(*args)
146 146 @file.read(*args)
147 147 end
148 148
149 149 def description
150 150 read_attribute(:description).to_s.slice(0,255)
151 151 end
152 152
153 153 private
154 154 def trac_fullpath
155 155 attachment_type = read_attribute(:type)
156 156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 158 end
159 159 end
160 160
161 161 class TracTicket < ActiveRecord::Base
162 162 self.table_name = :ticket
163 163 set_inheritance_column :none
164 164
165 165 # ticket changes: only migrate status changes and comments
166 166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168 168
169 169 def attachments
170 170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171 171 end
172 172
173 173 def ticket_type
174 174 read_attribute(:type)
175 175 end
176 176
177 177 def summary
178 178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 179 end
180 180
181 181 def description
182 182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 183 end
184 184
185 185 def time; Time.at(read_attribute(:time)) end
186 186 def changetime; Time.at(read_attribute(:changetime)) end
187 187 end
188 188
189 189 class TracTicketChange < ActiveRecord::Base
190 190 self.table_name = :ticket_change
191 191
192 192 def self.columns
193 193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194 194 super.select {|column| column.name.to_s != 'field'}
195 195 end
196 196
197 197 def time; Time.at(read_attribute(:time)) end
198 198 end
199 199
200 200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201 201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202 202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203 203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204 204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205 205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206 206 CamelCase TitleIndex)
207 207
208 208 class TracWikiPage < ActiveRecord::Base
209 209 self.table_name = :wiki
210 210 set_primary_key :name
211 211
212 212 def self.columns
213 213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 214 super.select {|column| column.name.to_s != 'readonly'}
215 215 end
216 216
217 217 def attachments
218 218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219 219 end
220 220
221 221 def time; Time.at(read_attribute(:time)) end
222 222 end
223 223
224 224 class TracPermission < ActiveRecord::Base
225 225 self.table_name = :permission
226 226 end
227 227
228 228 class TracSessionAttribute < ActiveRecord::Base
229 229 self.table_name = :session_attribute
230 230 end
231 231
232 232 def self.find_or_create_user(username, project_member = false)
233 233 return User.anonymous if username.blank?
234 234
235 235 u = User.find_by_login(username)
236 236 if !u
237 237 # Create a new user if not found
238 238 mail = username[0, User::MAIL_LENGTH_LIMIT]
239 239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240 240 mail = mail_attr.value
241 241 end
242 242 mail = "#{mail}@foo.bar" unless mail.include?("@")
243 243
244 244 name = username
245 245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246 246 name = name_attr.value
247 247 end
248 248 name =~ (/(.*)(\s+\w+)?/)
249 249 fn = $1.strip
250 250 ln = ($2 || '-').strip
251 251
252 252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253 253 :firstname => fn[0, limit_for(User, 'firstname')],
254 254 :lastname => ln[0, limit_for(User, 'lastname')]
255 255
256 256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257 257 u.password = 'trac'
258 258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259 259 # finally, a default user is used if the new user is not valid
260 260 u = User.first unless u.save
261 261 end
262 262 # Make sure he is a member of the project
263 263 if project_member && !u.member_of?(@target_project)
264 264 role = DEFAULT_ROLE
265 265 if u.admin
266 266 role = ROLE_MAPPING['admin']
267 267 elsif TracPermission.find_by_username_and_action(username, 'developer')
268 268 role = ROLE_MAPPING['developer']
269 269 end
270 270 Member.create(:user => u, :project => @target_project, :roles => [role])
271 271 u.reload
272 272 end
273 273 u
274 274 end
275 275
276 276 # Basic wiki syntax conversion
277 277 def self.convert_wiki_text(text)
278 278 # Titles
279 279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280 280 # External Links
281 281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282 282 # Ticket links:
283 283 # [ticket:234 Text],[ticket:234 This is a test]
284 284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285 285 # ticket:1234
286 286 # #1 is working cause Redmine uses the same syntax.
287 287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288 288 # Milestone links:
289 289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290 290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
291 291 # cause Redmine's wiki does not support this.
292 292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293 293 # [milestone:"0.1.0 Mercury"]
294 294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295 295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296 296 # milestone:0.1.0
297 297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298 298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299 299 # Internal Links
300 300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301 301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305 305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306 306
307 307 # Links to pages UsingJustWikiCaps
308 308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309 309 # Normalize things that were supposed to not be links
310 310 # like !NotALink
311 311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312 312 # Revisions links
313 313 text = text.gsub(/\[(\d+)\]/, 'r\1')
314 314 # Ticket number re-writing
315 315 text = text.gsub(/#(\d+)/) do |s|
316 316 if $1.length < 10
317 317 # TICKET_MAP[$1.to_i] ||= $1
318 318 "\##{TICKET_MAP[$1.to_i] || $1}"
319 319 else
320 320 s
321 321 end
322 322 end
323 323 # We would like to convert the Code highlighting too
324 324 # This will go into the next line.
325 325 shebang_line = false
326 326 # Reguar expression for start of code
327 327 pre_re = /\{\{\{/
328 328 # Code hightlighing...
329 329 shebang_re = /^\#\!([a-z]+)/
330 330 # Regular expression for end of code
331 331 pre_end_re = /\}\}\}/
332 332
333 333 # Go through the whole text..extract it line by line
334 334 text = text.gsub(/^(.*)$/) do |line|
335 335 m_pre = pre_re.match(line)
336 336 if m_pre
337 337 line = '<pre>'
338 338 else
339 339 m_sl = shebang_re.match(line)
340 340 if m_sl
341 341 shebang_line = true
342 342 line = '<code class="' + m_sl[1] + '">'
343 343 end
344 344 m_pre_end = pre_end_re.match(line)
345 345 if m_pre_end
346 346 line = '</pre>'
347 347 if shebang_line
348 348 line = '</code>' + line
349 349 end
350 350 end
351 351 end
352 352 line
353 353 end
354 354
355 355 # Highlighting
356 356 text = text.gsub(/'''''([^\s])/, '_*\1')
357 357 text = text.gsub(/([^\s])'''''/, '\1*_')
358 358 text = text.gsub(/'''/, '*')
359 359 text = text.gsub(/''/, '_')
360 360 text = text.gsub(/__/, '+')
361 361 text = text.gsub(/~~/, '-')
362 362 text = text.gsub(/`/, '@')
363 363 text = text.gsub(/,,/, '~')
364 364 # Lists
365 365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366 366
367 367 text
368 368 end
369 369
370 370 def self.migrate
371 371 establish_connection
372 372
373 373 # Quick database test
374 374 TracComponent.count
375 375
376 376 migrated_components = 0
377 377 migrated_milestones = 0
378 378 migrated_tickets = 0
379 379 migrated_custom_values = 0
380 380 migrated_ticket_attachments = 0
381 381 migrated_wiki_edits = 0
382 382 migrated_wiki_attachments = 0
383 383
384 384 #Wiki system initializing...
385 385 @target_project.wiki.destroy if @target_project.wiki
386 386 @target_project.reload
387 387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388 388 wiki_edit_count = 0
389 389
390 390 # Components
391 391 print "Migrating components"
392 392 issues_category_map = {}
393 393 TracComponent.all.each do |component|
394 394 print '.'
395 395 STDOUT.flush
396 396 c = IssueCategory.new :project => @target_project,
397 397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398 398 next unless c.save
399 399 issues_category_map[component.name] = c
400 400 migrated_components += 1
401 401 end
402 402 puts
403 403
404 404 # Milestones
405 405 print "Migrating milestones"
406 406 version_map = {}
407 407 TracMilestone.all.each do |milestone|
408 408 print '.'
409 409 STDOUT.flush
410 410 # First we try to find the wiki page...
411 411 p = wiki.find_or_new_page(milestone.name.to_s)
412 412 p.content = WikiContent.new(:page => p) if p.new_record?
413 413 p.content.text = milestone.description.to_s
414 414 p.content.author = find_or_create_user('trac')
415 415 p.content.comments = 'Milestone'
416 416 p.save
417 417
418 418 v = Version.new :project => @target_project,
419 419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420 420 :description => nil,
421 421 :wiki_page_title => milestone.name.to_s,
422 422 :effective_date => milestone.completed
423 423
424 424 next unless v.save
425 425 version_map[milestone.name] = v
426 426 migrated_milestones += 1
427 427 end
428 428 puts
429 429
430 430 # Custom fields
431 431 # TODO: read trac.ini instead
432 432 print "Migrating custom fields"
433 433 custom_field_map = {}
434 434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435 435 print '.'
436 436 STDOUT.flush
437 437 # Redmine custom field name
438 438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439 439 # Find if the custom already exists in Redmine
440 440 f = IssueCustomField.find_by_name(field_name)
441 441 # Or create a new one
442 442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443 443 :field_format => 'string')
444 444
445 445 next if f.new_record?
446 446 f.trackers = Tracker.all
447 447 f.projects << @target_project
448 448 custom_field_map[field.name] = f
449 449 end
450 450 puts
451 451
452 452 # Trac 'resolution' field as a Redmine custom field
453 453 r = IssueCustomField.where(:name => "Resolution").first
454 454 r = IssueCustomField.new(:name => 'Resolution',
455 455 :field_format => 'list',
456 456 :is_filter => true) if r.nil?
457 457 r.trackers = Tracker.all
458 458 r.projects << @target_project
459 459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460 460 r.save!
461 461 custom_field_map['resolution'] = r
462 462
463 463 # Tickets
464 464 print "Migrating tickets"
465 465 TracTicket.find_each(:batch_size => 200) do |ticket|
466 466 print '.'
467 467 STDOUT.flush
468 468 i = Issue.new :project => @target_project,
469 469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470 470 :description => convert_wiki_text(encode(ticket.description)),
471 471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472 472 :created_on => ticket.time
473 473 i.author = find_or_create_user(ticket.reporter)
474 474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475 475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476 476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477 477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478 478 i.id = ticket.id unless Issue.exists?(ticket.id)
479 479 next unless Time.fake(ticket.changetime) { i.save }
480 480 TICKET_MAP[ticket.id] = i.id
481 481 migrated_tickets += 1
482 482
483 483 # Owner
484 484 unless ticket.owner.blank?
485 485 i.assigned_to = find_or_create_user(ticket.owner, true)
486 486 Time.fake(ticket.changetime) { i.save }
487 487 end
488 488
489 489 # Comments and status/resolution changes
490 490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491 491 status_change = changeset.select {|change| change.field == 'status'}.first
492 492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493 493 comment_change = changeset.select {|change| change.field == 'comment'}.first
494 494
495 495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496 496 :created_on => time
497 497 n.user = find_or_create_user(changeset.first.author)
498 498 n.journalized = i
499 499 if status_change &&
500 500 STATUS_MAPPING[status_change.oldvalue] &&
501 501 STATUS_MAPPING[status_change.newvalue] &&
502 502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503 503 n.details << JournalDetail.new(:property => 'attr',
504 504 :prop_key => 'status_id',
505 505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506 506 :value => STATUS_MAPPING[status_change.newvalue].id)
507 507 end
508 508 if resolution_change
509 509 n.details << JournalDetail.new(:property => 'cf',
510 510 :prop_key => custom_field_map['resolution'].id,
511 511 :old_value => resolution_change.oldvalue,
512 512 :value => resolution_change.newvalue)
513 513 end
514 514 n.save unless n.details.empty? && n.notes.blank?
515 515 end
516 516
517 517 # Attachments
518 518 ticket.attachments.each do |attachment|
519 519 next unless attachment.exist?
520 520 attachment.open {
521 521 a = Attachment.new :created_on => attachment.time
522 522 a.file = attachment
523 523 a.author = find_or_create_user(attachment.author)
524 524 a.container = i
525 525 a.description = attachment.description
526 526 migrated_ticket_attachments += 1 if a.save
527 527 }
528 528 end
529 529
530 530 # Custom fields
531 531 custom_values = ticket.customs.inject({}) do |h, custom|
532 532 if custom_field = custom_field_map[custom.name]
533 533 h[custom_field.id] = custom.value
534 534 migrated_custom_values += 1
535 535 end
536 536 h
537 537 end
538 538 if custom_field_map['resolution'] && !ticket.resolution.blank?
539 539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
540 540 end
541 541 i.custom_field_values = custom_values
542 542 i.save_custom_field_values
543 543 end
544 544
545 545 # update issue id sequence if needed (postgresql)
546 546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547 547 puts
548 548
549 549 # Wiki
550 550 print "Migrating wiki"
551 551 if wiki.save
552 552 TracWikiPage.order('name, version').all.each do |page|
553 553 # Do not migrate Trac manual wiki pages
554 554 next if TRAC_WIKI_PAGES.include?(page.name)
555 555 wiki_edit_count += 1
556 556 print '.'
557 557 STDOUT.flush
558 558 p = wiki.find_or_new_page(page.name)
559 559 p.content = WikiContent.new(:page => p) if p.new_record?
560 560 p.content.text = page.text
561 561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562 562 p.content.comments = page.comment
563 563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564 564
565 565 next if p.content.new_record?
566 566 migrated_wiki_edits += 1
567 567
568 568 # Attachments
569 569 page.attachments.each do |attachment|
570 570 next unless attachment.exist?
571 571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572 572 attachment.open {
573 573 a = Attachment.new :created_on => attachment.time
574 574 a.file = attachment
575 575 a.author = find_or_create_user(attachment.author)
576 576 a.description = attachment.description
577 577 a.container = p
578 578 migrated_wiki_attachments += 1 if a.save
579 579 }
580 580 end
581 581 end
582 582
583 583 wiki.reload
584 584 wiki.pages.each do |page|
585 585 page.content.text = convert_wiki_text(page.content.text)
586 586 Time.fake(page.content.updated_on) { page.content.save }
587 587 end
588 588 end
589 589 puts
590 590
591 591 puts
592 592 puts "Components: #{migrated_components}/#{TracComponent.count}"
593 593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
594 594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
595 595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596 596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
597 597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
598 598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599 599 end
600 600
601 601 def self.limit_for(klass, attribute)
602 602 klass.columns_hash[attribute.to_s].limit
603 603 end
604 604
605 605 def self.encoding(charset)
606 606 @ic = Iconv.new('UTF-8', charset)
607 607 rescue Iconv::InvalidEncoding
608 608 puts "Invalid encoding!"
609 609 return false
610 610 end
611 611
612 612 def self.set_trac_directory(path)
613 613 @@trac_directory = path
614 614 raise "This directory doesn't exist!" unless File.directory?(path)
615 615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616 616 @@trac_directory
617 617 rescue Exception => e
618 618 puts e
619 619 return false
620 620 end
621 621
622 622 def self.trac_directory
623 623 @@trac_directory
624 624 end
625 625
626 626 def self.set_trac_adapter(adapter)
627 627 return false if adapter.blank?
628 628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629 629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
630 630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631 631 @@trac_adapter = adapter
632 632 rescue Exception => e
633 633 puts e
634 634 return false
635 635 end
636 636
637 637 def self.set_trac_db_host(host)
638 638 return nil if host.blank?
639 639 @@trac_db_host = host
640 640 end
641 641
642 642 def self.set_trac_db_port(port)
643 643 return nil if port.to_i == 0
644 644 @@trac_db_port = port.to_i
645 645 end
646 646
647 647 def self.set_trac_db_name(name)
648 648 return nil if name.blank?
649 649 @@trac_db_name = name
650 650 end
651 651
652 652 def self.set_trac_db_username(username)
653 653 @@trac_db_username = username
654 654 end
655 655
656 656 def self.set_trac_db_password(password)
657 657 @@trac_db_password = password
658 658 end
659 659
660 660 def self.set_trac_db_schema(schema)
661 661 @@trac_db_schema = schema
662 662 end
663 663
664 664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665 665
666 666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667 667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668 668
669 669 def self.target_project_identifier(identifier)
670 670 project = Project.find_by_identifier(identifier)
671 671 if !project
672 672 # create the target project
673 673 project = Project.new :name => identifier.humanize,
674 674 :description => ''
675 675 project.identifier = identifier
676 676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677 677 # enable issues and wiki for the created project
678 678 project.enabled_module_names = ['issue_tracking', 'wiki']
679 679 else
680 680 puts
681 681 puts "This project already exists in your Redmine database."
682 682 print "Are you sure you want to append data to this project ? [Y/n] "
683 683 STDOUT.flush
684 684 exit if STDIN.gets.match(/^n$/i)
685 685 end
686 686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687 687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688 688 @target_project = project.new_record? ? nil : project
689 689 @target_project.reload
690 690 end
691 691
692 692 def self.connection_params
693 693 if trac_adapter == 'sqlite3'
694 694 {:adapter => 'sqlite3',
695 695 :database => trac_db_path}
696 696 else
697 697 {:adapter => trac_adapter,
698 698 :database => trac_db_name,
699 699 :host => trac_db_host,
700 700 :port => trac_db_port,
701 701 :username => trac_db_username,
702 702 :password => trac_db_password,
703 703 :schema_search_path => trac_db_schema
704 704 }
705 705 end
706 706 end
707 707
708 708 def self.establish_connection
709 709 constants.each do |const|
710 710 klass = const_get(const)
711 711 next unless klass.respond_to? 'establish_connection'
712 712 klass.establish_connection connection_params
713 713 end
714 714 end
715 715
716 716 private
717 717 def self.encode(text)
718 718 @ic.iconv text
719 719 rescue
720 720 text
721 721 end
722 722 end
723 723
724 724 puts
725 725 if Redmine::DefaultData::Loader.no_data?
726 726 puts "Redmine configuration need to be loaded before importing data."
727 727 puts "Please, run this first:"
728 728 puts
729 729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730 730 exit
731 731 end
732 732
733 733 puts "WARNING: a new project will be added to Redmine during this process."
734 734 print "Are you sure you want to continue ? [y/N] "
735 735 STDOUT.flush
736 736 break unless STDIN.gets.match(/^y$/i)
737 737 puts
738 738
739 739 def prompt(text, options = {}, &block)
740 740 default = options[:default] || ''
741 741 while true
742 742 print "#{text} [#{default}]: "
743 743 STDOUT.flush
744 744 value = STDIN.gets.chomp!
745 745 value = default if value.blank?
746 746 break if yield value
747 747 end
748 748 end
749 749
750 750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751 751
752 752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753 753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754 754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755 755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756 756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757 757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758 758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759 759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760 760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761 761 end
762 762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763 763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764 764 puts
765 765
766 766 # Turn off email notifications
767 767 Setting.notified_events = []
768 768
769 769 TracMigrate.migrate
770 770 end
771 771 end
772 772
@@ -1,185 +1,185
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 begin
21 21 require 'mocha'
22 22 rescue
23 23 # Won't run some tests
24 24 end
25 25
26 26 class AccountTest < ActionController::IntegrationTest
27 27 fixtures :users, :roles
28 28
29 29 # Replace this with your real tests.
30 30 def test_login
31 31 get "my/page"
32 32 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
33 33 log_user('jsmith', 'jsmith')
34 34
35 35 get "my/account"
36 36 assert_response :success
37 37 assert_template "my/account"
38 38 end
39 39
40 40 def test_autologin
41 41 user = User.find(1)
42 42 Setting.autologin = "7"
43 43 Token.delete_all
44 44
45 45 # User logs in with 'autologin' checked
46 46 post '/login', :username => user.login, :password => 'admin', :autologin => 1
47 47 assert_redirected_to '/my/page'
48 token = Token.find :first
48 token = Token.first
49 49 assert_not_nil token
50 50 assert_equal user, token.user
51 51 assert_equal 'autologin', token.action
52 52 assert_equal user.id, session[:user_id]
53 53 assert_equal token.value, cookies['autologin']
54 54
55 55 # Session is cleared
56 56 reset!
57 57 User.current = nil
58 58 # Clears user's last login timestamp
59 59 user.update_attribute :last_login_on, nil
60 60 assert_nil user.reload.last_login_on
61 61
62 62 # User comes back with his autologin cookie
63 63 cookies[:autologin] = token.value
64 64 get '/my/page'
65 65 assert_response :success
66 66 assert_template 'my/page'
67 67 assert_equal user.id, session[:user_id]
68 68 assert_not_nil user.reload.last_login_on
69 69 end
70 70
71 71 def test_lost_password
72 72 Token.delete_all
73 73
74 74 get "account/lost_password"
75 75 assert_response :success
76 76 assert_template "account/lost_password"
77 77 assert_select 'input[name=mail]'
78 78
79 79 post "account/lost_password", :mail => 'jSmith@somenet.foo'
80 80 assert_redirected_to "/login"
81 81
82 82 token = Token.first
83 83 assert_equal 'recovery', token.action
84 84 assert_equal 'jsmith@somenet.foo', token.user.mail
85 85 assert !token.expired?
86 86
87 87 get "account/lost_password", :token => token.value
88 88 assert_response :success
89 89 assert_template "account/password_recovery"
90 90 assert_select 'input[type=hidden][name=token][value=?]', token.value
91 91 assert_select 'input[name=new_password]'
92 92 assert_select 'input[name=new_password_confirmation]'
93 93
94 94 post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
95 95 assert_redirected_to "/login"
96 96 assert_equal 'Password was successfully updated.', flash[:notice]
97 97
98 98 log_user('jsmith', 'newpass123')
99 99 assert_equal 0, Token.count
100 100 end
101 101
102 102 def test_register_with_automatic_activation
103 103 Setting.self_registration = '3'
104 104
105 105 get 'account/register'
106 106 assert_response :success
107 107 assert_template 'account/register'
108 108
109 109 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
110 110 :password => "newpass123", :password_confirmation => "newpass123"}
111 111 assert_redirected_to '/my/account'
112 112 follow_redirect!
113 113 assert_response :success
114 114 assert_template 'my/account'
115 115
116 116 user = User.find_by_login('newuser')
117 117 assert_not_nil user
118 118 assert user.active?
119 119 assert_not_nil user.last_login_on
120 120 end
121 121
122 122 def test_register_with_manual_activation
123 123 Setting.self_registration = '2'
124 124
125 125 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
126 126 :password => "newpass123", :password_confirmation => "newpass123"}
127 127 assert_redirected_to '/login'
128 128 assert !User.find_by_login('newuser').active?
129 129 end
130 130
131 131 def test_register_with_email_activation
132 132 Setting.self_registration = '1'
133 133 Token.delete_all
134 134
135 135 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
136 136 :password => "newpass123", :password_confirmation => "newpass123"}
137 137 assert_redirected_to '/login'
138 138 assert !User.find_by_login('newuser').active?
139 139
140 140 token = Token.first
141 141 assert_equal 'register', token.action
142 142 assert_equal 'newuser@foo.bar', token.user.mail
143 143 assert !token.expired?
144 144
145 145 get 'account/activate', :token => token.value
146 146 assert_redirected_to '/login'
147 147 log_user('newuser', 'newpass123')
148 148 end
149 149
150 150 def test_onthefly_registration
151 151 # disable registration
152 152 Setting.self_registration = '0'
153 153 AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66})
154 154
155 155 post '/login', :username => 'foo', :password => 'bar'
156 156 assert_redirected_to '/my/page'
157 157
158 158 user = User.find_by_login('foo')
159 159 assert user.is_a?(User)
160 160 assert_equal 66, user.auth_source_id
161 161 assert user.hashed_password.blank?
162 162 end
163 163
164 164 def test_onthefly_registration_with_invalid_attributes
165 165 # disable registration
166 166 Setting.self_registration = '0'
167 167 AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66})
168 168
169 169 post '/login', :username => 'foo', :password => 'bar'
170 170 assert_response :success
171 171 assert_template 'account/register'
172 172 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
173 173 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
174 174 assert_no_tag :input, :attributes => { :name => 'user[login]' }
175 175 assert_no_tag :input, :attributes => { :name => 'user[password]' }
176 176
177 177 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
178 178 assert_redirected_to '/my/account'
179 179
180 180 user = User.find_by_login('foo')
181 181 assert user.is_a?(User)
182 182 assert_equal 66, user.auth_source_id
183 183 assert user.hashed_password.blank?
184 184 end
185 185 end
@@ -1,1938 +1,1938
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues, :journals, :journal_details,
29 29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 30 :time_entries
31 31
32 32 include Redmine::I18n
33 33
34 34 def teardown
35 35 User.current = nil
36 36 end
37 37
38 38 def test_create
39 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
40 40 :status_id => 1, :priority => IssuePriority.all.first,
41 41 :subject => 'test_create',
42 42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
43 43 assert issue.save
44 44 issue.reload
45 45 assert_equal 1.5, issue.estimated_hours
46 46 end
47 47
48 48 def test_create_minimal
49 49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
50 50 :status_id => 1, :priority => IssuePriority.all.first,
51 51 :subject => 'test_create')
52 52 assert issue.save
53 53 assert issue.description.nil?
54 54 assert_nil issue.estimated_hours
55 55 end
56 56
57 57 def test_start_date_format_should_be_validated
58 58 set_language_if_valid 'en'
59 59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
60 60 issue = Issue.new(:start_date => invalid_date)
61 61 assert !issue.valid?
62 62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
63 63 end
64 64 end
65 65
66 66 def test_due_date_format_should_be_validated
67 67 set_language_if_valid 'en'
68 68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
69 69 issue = Issue.new(:due_date => invalid_date)
70 70 assert !issue.valid?
71 71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
72 72 end
73 73 end
74 74
75 75 def test_due_date_lesser_than_start_date_should_not_validate
76 76 set_language_if_valid 'en'
77 77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
78 78 assert !issue.valid?
79 79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
80 80 end
81 81
82 82 def test_create_with_required_custom_field
83 83 set_language_if_valid 'en'
84 84 field = IssueCustomField.find_by_name('Database')
85 85 field.update_attribute(:is_required, true)
86 86
87 87 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
88 88 :status_id => 1, :subject => 'test_create',
89 89 :description => 'IssueTest#test_create_with_required_custom_field')
90 90 assert issue.available_custom_fields.include?(field)
91 91 # No value for the custom field
92 92 assert !issue.save
93 93 assert_equal ["Database can't be blank"], issue.errors.full_messages
94 94 # Blank value
95 95 issue.custom_field_values = { field.id => '' }
96 96 assert !issue.save
97 97 assert_equal ["Database can't be blank"], issue.errors.full_messages
98 98 # Invalid value
99 99 issue.custom_field_values = { field.id => 'SQLServer' }
100 100 assert !issue.save
101 101 assert_equal ["Database is not included in the list"], issue.errors.full_messages
102 102 # Valid value
103 103 issue.custom_field_values = { field.id => 'PostgreSQL' }
104 104 assert issue.save
105 105 issue.reload
106 106 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
107 107 end
108 108
109 109 def test_create_with_group_assignment
110 110 with_settings :issue_group_assignment => '1' do
111 111 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
112 112 :subject => 'Group assignment',
113 113 :assigned_to_id => 11).save
114 114 issue = Issue.first(:order => 'id DESC')
115 115 assert_kind_of Group, issue.assigned_to
116 116 assert_equal Group.find(11), issue.assigned_to
117 117 end
118 118 end
119 119
120 120 def test_create_with_parent_issue_id
121 121 issue = Issue.new(:project_id => 1, :tracker_id => 1,
122 122 :author_id => 1, :subject => 'Group assignment',
123 123 :parent_issue_id => 1)
124 124 assert_save issue
125 125 assert_equal 1, issue.parent_issue_id
126 126 assert_equal Issue.find(1), issue.parent
127 127 end
128 128
129 129 def test_create_with_sharp_parent_issue_id
130 130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
131 131 :author_id => 1, :subject => 'Group assignment',
132 132 :parent_issue_id => "#1")
133 133 assert_save issue
134 134 assert_equal 1, issue.parent_issue_id
135 135 assert_equal Issue.find(1), issue.parent
136 136 end
137 137
138 138 def test_create_with_invalid_parent_issue_id
139 139 set_language_if_valid 'en'
140 140 issue = Issue.new(:project_id => 1, :tracker_id => 1,
141 141 :author_id => 1, :subject => 'Group assignment',
142 142 :parent_issue_id => '01ABC')
143 143 assert !issue.save
144 144 assert_equal '01ABC', issue.parent_issue_id
145 145 assert_include 'Parent task is invalid', issue.errors.full_messages
146 146 end
147 147
148 148 def test_create_with_invalid_sharp_parent_issue_id
149 149 set_language_if_valid 'en'
150 150 issue = Issue.new(:project_id => 1, :tracker_id => 1,
151 151 :author_id => 1, :subject => 'Group assignment',
152 152 :parent_issue_id => '#01ABC')
153 153 assert !issue.save
154 154 assert_equal '#01ABC', issue.parent_issue_id
155 155 assert_include 'Parent task is invalid', issue.errors.full_messages
156 156 end
157 157
158 158 def assert_visibility_match(user, issues)
159 159 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
160 160 end
161 161
162 162 def test_visible_scope_for_anonymous
163 163 # Anonymous user should see issues of public projects only
164 164 issues = Issue.visible(User.anonymous).all
165 165 assert issues.any?
166 166 assert_nil issues.detect {|issue| !issue.project.is_public?}
167 167 assert_nil issues.detect {|issue| issue.is_private?}
168 168 assert_visibility_match User.anonymous, issues
169 169 end
170 170
171 171 def test_visible_scope_for_anonymous_without_view_issues_permissions
172 172 # Anonymous user should not see issues without permission
173 173 Role.anonymous.remove_permission!(:view_issues)
174 174 issues = Issue.visible(User.anonymous).all
175 175 assert issues.empty?
176 176 assert_visibility_match User.anonymous, issues
177 177 end
178 178
179 179 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
180 180 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
181 181 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
182 182 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
183 183 assert !issue.visible?(User.anonymous)
184 184 end
185 185
186 186 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
187 187 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
188 188 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
189 189 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
190 190 assert !issue.visible?(User.anonymous)
191 191 end
192 192
193 193 def test_visible_scope_for_non_member
194 194 user = User.find(9)
195 195 assert user.projects.empty?
196 196 # Non member user should see issues of public projects only
197 197 issues = Issue.visible(user).all
198 198 assert issues.any?
199 199 assert_nil issues.detect {|issue| !issue.project.is_public?}
200 200 assert_nil issues.detect {|issue| issue.is_private?}
201 201 assert_visibility_match user, issues
202 202 end
203 203
204 204 def test_visible_scope_for_non_member_with_own_issues_visibility
205 205 Role.non_member.update_attribute :issues_visibility, 'own'
206 206 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
207 207 user = User.find(9)
208 208
209 209 issues = Issue.visible(user).all
210 210 assert issues.any?
211 211 assert_nil issues.detect {|issue| issue.author != user}
212 212 assert_visibility_match user, issues
213 213 end
214 214
215 215 def test_visible_scope_for_non_member_without_view_issues_permissions
216 216 # Non member user should not see issues without permission
217 217 Role.non_member.remove_permission!(:view_issues)
218 218 user = User.find(9)
219 219 assert user.projects.empty?
220 220 issues = Issue.visible(user).all
221 221 assert issues.empty?
222 222 assert_visibility_match user, issues
223 223 end
224 224
225 225 def test_visible_scope_for_member
226 226 user = User.find(9)
227 227 # User should see issues of projects for which he has view_issues permissions only
228 228 Role.non_member.remove_permission!(:view_issues)
229 229 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
230 230 issues = Issue.visible(user).all
231 231 assert issues.any?
232 232 assert_nil issues.detect {|issue| issue.project_id != 3}
233 233 assert_nil issues.detect {|issue| issue.is_private?}
234 234 assert_visibility_match user, issues
235 235 end
236 236
237 237 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
238 238 user = User.find(8)
239 239 assert user.groups.any?
240 240 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
241 241 Role.non_member.remove_permission!(:view_issues)
242 242
243 243 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
244 244 :status_id => 1, :priority => IssuePriority.all.first,
245 245 :subject => 'Assignment test',
246 246 :assigned_to => user.groups.first,
247 247 :is_private => true)
248 248
249 249 Role.find(2).update_attribute :issues_visibility, 'default'
250 250 issues = Issue.visible(User.find(8)).all
251 251 assert issues.any?
252 252 assert issues.include?(issue)
253 253
254 254 Role.find(2).update_attribute :issues_visibility, 'own'
255 255 issues = Issue.visible(User.find(8)).all
256 256 assert issues.any?
257 257 assert issues.include?(issue)
258 258 end
259 259
260 260 def test_visible_scope_for_admin
261 261 user = User.find(1)
262 262 user.members.each(&:destroy)
263 263 assert user.projects.empty?
264 264 issues = Issue.visible(user).all
265 265 assert issues.any?
266 266 # Admin should see issues on private projects that he does not belong to
267 267 assert issues.detect {|issue| !issue.project.is_public?}
268 268 # Admin should see private issues of other users
269 269 assert issues.detect {|issue| issue.is_private? && issue.author != user}
270 270 assert_visibility_match user, issues
271 271 end
272 272
273 273 def test_visible_scope_with_project
274 274 project = Project.find(1)
275 275 issues = Issue.visible(User.find(2), :project => project).all
276 276 projects = issues.collect(&:project).uniq
277 277 assert_equal 1, projects.size
278 278 assert_equal project, projects.first
279 279 end
280 280
281 281 def test_visible_scope_with_project_and_subprojects
282 282 project = Project.find(1)
283 283 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
284 284 projects = issues.collect(&:project).uniq
285 285 assert projects.size > 1
286 286 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
287 287 end
288 288
289 289 def test_visible_and_nested_set_scopes
290 290 assert_equal 0, Issue.find(1).descendants.visible.all.size
291 291 end
292 292
293 293 def test_open_scope
294 294 issues = Issue.open.all
295 295 assert_nil issues.detect(&:closed?)
296 296 end
297 297
298 298 def test_open_scope_with_arg
299 299 issues = Issue.open(false).all
300 300 assert_equal issues, issues.select(&:closed?)
301 301 end
302 302
303 303 def test_errors_full_messages_should_include_custom_fields_errors
304 304 field = IssueCustomField.find_by_name('Database')
305 305
306 306 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
307 307 :status_id => 1, :subject => 'test_create',
308 308 :description => 'IssueTest#test_create_with_required_custom_field')
309 309 assert issue.available_custom_fields.include?(field)
310 310 # Invalid value
311 311 issue.custom_field_values = { field.id => 'SQLServer' }
312 312
313 313 assert !issue.valid?
314 314 assert_equal 1, issue.errors.full_messages.size
315 315 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
316 316 issue.errors.full_messages.first
317 317 end
318 318
319 319 def test_update_issue_with_required_custom_field
320 320 field = IssueCustomField.find_by_name('Database')
321 321 field.update_attribute(:is_required, true)
322 322
323 323 issue = Issue.find(1)
324 324 assert_nil issue.custom_value_for(field)
325 325 assert issue.available_custom_fields.include?(field)
326 326 # No change to custom values, issue can be saved
327 327 assert issue.save
328 328 # Blank value
329 329 issue.custom_field_values = { field.id => '' }
330 330 assert !issue.save
331 331 # Valid value
332 332 issue.custom_field_values = { field.id => 'PostgreSQL' }
333 333 assert issue.save
334 334 issue.reload
335 335 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
336 336 end
337 337
338 338 def test_should_not_update_attributes_if_custom_fields_validation_fails
339 339 issue = Issue.find(1)
340 340 field = IssueCustomField.find_by_name('Database')
341 341 assert issue.available_custom_fields.include?(field)
342 342
343 343 issue.custom_field_values = { field.id => 'Invalid' }
344 344 issue.subject = 'Should be not be saved'
345 345 assert !issue.save
346 346
347 347 issue.reload
348 348 assert_equal "Can't print recipes", issue.subject
349 349 end
350 350
351 351 def test_should_not_recreate_custom_values_objects_on_update
352 352 field = IssueCustomField.find_by_name('Database')
353 353
354 354 issue = Issue.find(1)
355 355 issue.custom_field_values = { field.id => 'PostgreSQL' }
356 356 assert issue.save
357 357 custom_value = issue.custom_value_for(field)
358 358 issue.reload
359 359 issue.custom_field_values = { field.id => 'MySQL' }
360 360 assert issue.save
361 361 issue.reload
362 362 assert_equal custom_value.id, issue.custom_value_for(field).id
363 363 end
364 364
365 365 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
366 366 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
367 367 :status_id => 1, :subject => 'Test',
368 368 :custom_field_values => {'2' => 'Test'})
369 369 assert !Tracker.find(2).custom_field_ids.include?(2)
370 370
371 371 issue = Issue.find(issue.id)
372 372 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
373 373
374 374 issue = Issue.find(issue.id)
375 375 custom_value = issue.custom_value_for(2)
376 376 assert_not_nil custom_value
377 377 assert_equal 'Test', custom_value.value
378 378 end
379 379
380 380 def test_assigning_tracker_id_should_reload_custom_fields_values
381 381 issue = Issue.new(:project => Project.find(1))
382 382 assert issue.custom_field_values.empty?
383 383 issue.tracker_id = 1
384 384 assert issue.custom_field_values.any?
385 385 end
386 386
387 387 def test_assigning_attributes_should_assign_project_and_tracker_first
388 388 seq = sequence('seq')
389 389 issue = Issue.new
390 390 issue.expects(:project_id=).in_sequence(seq)
391 391 issue.expects(:tracker_id=).in_sequence(seq)
392 392 issue.expects(:subject=).in_sequence(seq)
393 393 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
394 394 end
395 395
396 396 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
397 397 attributes = ActiveSupport::OrderedHash.new
398 398 attributes['custom_field_values'] = { '1' => 'MySQL' }
399 399 attributes['tracker_id'] = '1'
400 400 issue = Issue.new(:project => Project.find(1))
401 401 issue.attributes = attributes
402 402 assert_equal 'MySQL', issue.custom_field_value(1)
403 403 end
404 404
405 405 def test_should_update_issue_with_disabled_tracker
406 406 p = Project.find(1)
407 407 issue = Issue.find(1)
408 408
409 409 p.trackers.delete(issue.tracker)
410 410 assert !p.trackers.include?(issue.tracker)
411 411
412 412 issue.reload
413 413 issue.subject = 'New subject'
414 414 assert issue.save
415 415 end
416 416
417 417 def test_should_not_set_a_disabled_tracker
418 418 p = Project.find(1)
419 419 p.trackers.delete(Tracker.find(2))
420 420
421 421 issue = Issue.find(1)
422 422 issue.tracker_id = 2
423 423 issue.subject = 'New subject'
424 424 assert !issue.save
425 425 assert_not_nil issue.errors[:tracker_id]
426 426 end
427 427
428 428 def test_category_based_assignment
429 429 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
430 430 :status_id => 1, :priority => IssuePriority.all.first,
431 431 :subject => 'Assignment test',
432 432 :description => 'Assignment test', :category_id => 1)
433 433 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
434 434 end
435 435
436 436 def test_new_statuses_allowed_to
437 437 WorkflowTransition.delete_all
438 438 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
439 439 :old_status_id => 1, :new_status_id => 2,
440 440 :author => false, :assignee => false)
441 441 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
442 442 :old_status_id => 1, :new_status_id => 3,
443 443 :author => true, :assignee => false)
444 444 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
445 445 :new_status_id => 4, :author => false,
446 446 :assignee => true)
447 447 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
448 448 :old_status_id => 1, :new_status_id => 5,
449 449 :author => true, :assignee => true)
450 450 status = IssueStatus.find(1)
451 451 role = Role.find(1)
452 452 tracker = Tracker.find(1)
453 453 user = User.find(2)
454 454
455 455 issue = Issue.generate!(:tracker => tracker, :status => status,
456 456 :project_id => 1, :author_id => 1)
457 457 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
458 458
459 459 issue = Issue.generate!(:tracker => tracker, :status => status,
460 460 :project_id => 1, :author => user)
461 461 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
462 462
463 463 issue = Issue.generate!(:tracker => tracker, :status => status,
464 464 :project_id => 1, :author_id => 1,
465 465 :assigned_to => user)
466 466 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
467 467
468 468 issue = Issue.generate!(:tracker => tracker, :status => status,
469 469 :project_id => 1, :author => user,
470 470 :assigned_to => user)
471 471 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
472 472 end
473 473
474 474 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
475 475 admin = User.find(1)
476 476 issue = Issue.find(1)
477 477 assert !admin.member_of?(issue.project)
478 478 expected_statuses = [issue.status] +
479 479 WorkflowTransition.find_all_by_old_status_id(
480 480 issue.status_id).map(&:new_status).uniq.sort
481 481 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
482 482 end
483 483
484 484 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
485 485 issue = Issue.find(1).copy
486 486 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
487 487
488 488 issue = Issue.find(2).copy
489 489 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
490 490 end
491 491
492 492 def test_safe_attributes_names_should_not_include_disabled_field
493 493 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
494 494
495 495 issue = Issue.new(:tracker => tracker)
496 496 assert_include 'tracker_id', issue.safe_attribute_names
497 497 assert_include 'status_id', issue.safe_attribute_names
498 498 assert_include 'subject', issue.safe_attribute_names
499 499 assert_include 'description', issue.safe_attribute_names
500 500 assert_include 'custom_field_values', issue.safe_attribute_names
501 501 assert_include 'custom_fields', issue.safe_attribute_names
502 502 assert_include 'lock_version', issue.safe_attribute_names
503 503
504 504 tracker.core_fields.each do |field|
505 505 assert_include field, issue.safe_attribute_names
506 506 end
507 507
508 508 tracker.disabled_core_fields.each do |field|
509 509 assert_not_include field, issue.safe_attribute_names
510 510 end
511 511 end
512 512
513 513 def test_safe_attributes_should_ignore_disabled_fields
514 514 tracker = Tracker.find(1)
515 515 tracker.core_fields = %w(assigned_to_id due_date)
516 516 tracker.save!
517 517
518 518 issue = Issue.new(:tracker => tracker)
519 519 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
520 520 assert_nil issue.start_date
521 521 assert_equal Date.parse('2012-07-14'), issue.due_date
522 522 end
523 523
524 524 def test_safe_attributes_should_accept_target_tracker_enabled_fields
525 525 source = Tracker.find(1)
526 526 source.core_fields = []
527 527 source.save!
528 528 target = Tracker.find(2)
529 529 target.core_fields = %w(assigned_to_id due_date)
530 530 target.save!
531 531
532 532 issue = Issue.new(:tracker => source)
533 533 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
534 534 assert_equal target, issue.tracker
535 535 assert_equal Date.parse('2012-07-14'), issue.due_date
536 536 end
537 537
538 538 def test_safe_attributes_should_not_include_readonly_fields
539 539 WorkflowPermission.delete_all
540 540 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
541 541 :role_id => 1, :field_name => 'due_date',
542 542 :rule => 'readonly')
543 543 user = User.find(2)
544 544
545 545 issue = Issue.new(:project_id => 1, :tracker_id => 1)
546 546 assert_equal %w(due_date), issue.read_only_attribute_names(user)
547 547 assert_not_include 'due_date', issue.safe_attribute_names(user)
548 548
549 549 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
550 550 assert_equal Date.parse('2012-07-14'), issue.start_date
551 551 assert_nil issue.due_date
552 552 end
553 553
554 554 def test_safe_attributes_should_not_include_readonly_custom_fields
555 555 cf1 = IssueCustomField.create!(:name => 'Writable field',
556 556 :field_format => 'string',
557 557 :is_for_all => true, :tracker_ids => [1])
558 558 cf2 = IssueCustomField.create!(:name => 'Readonly field',
559 559 :field_format => 'string',
560 560 :is_for_all => true, :tracker_ids => [1])
561 561 WorkflowPermission.delete_all
562 562 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
563 563 :role_id => 1, :field_name => cf2.id.to_s,
564 564 :rule => 'readonly')
565 565 user = User.find(2)
566 566 issue = Issue.new(:project_id => 1, :tracker_id => 1)
567 567 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
568 568 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
569 569
570 570 issue.send :safe_attributes=, {'custom_field_values' => {
571 571 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
572 572 }}, user
573 573 assert_equal 'value1', issue.custom_field_value(cf1)
574 574 assert_nil issue.custom_field_value(cf2)
575 575
576 576 issue.send :safe_attributes=, {'custom_fields' => [
577 577 {'id' => cf1.id.to_s, 'value' => 'valuea'},
578 578 {'id' => cf2.id.to_s, 'value' => 'valueb'}
579 579 ]}, user
580 580 assert_equal 'valuea', issue.custom_field_value(cf1)
581 581 assert_nil issue.custom_field_value(cf2)
582 582 end
583 583
584 584 def test_editable_custom_field_values_should_return_non_readonly_custom_values
585 585 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
586 586 :is_for_all => true, :tracker_ids => [1, 2])
587 587 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
588 588 :is_for_all => true, :tracker_ids => [1, 2])
589 589 WorkflowPermission.delete_all
590 590 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
591 591 :field_name => cf2.id.to_s, :rule => 'readonly')
592 592 user = User.find(2)
593 593
594 594 issue = Issue.new(:project_id => 1, :tracker_id => 1)
595 595 values = issue.editable_custom_field_values(user)
596 596 assert values.detect {|value| value.custom_field == cf1}
597 597 assert_nil values.detect {|value| value.custom_field == cf2}
598 598
599 599 issue.tracker_id = 2
600 600 values = issue.editable_custom_field_values(user)
601 601 assert values.detect {|value| value.custom_field == cf1}
602 602 assert values.detect {|value| value.custom_field == cf2}
603 603 end
604 604
605 605 def test_safe_attributes_should_accept_target_tracker_writable_fields
606 606 WorkflowPermission.delete_all
607 607 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
608 608 :role_id => 1, :field_name => 'due_date',
609 609 :rule => 'readonly')
610 610 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
611 611 :role_id => 1, :field_name => 'start_date',
612 612 :rule => 'readonly')
613 613 user = User.find(2)
614 614
615 615 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
616 616
617 617 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
618 618 'due_date' => '2012-07-14'}, user
619 619 assert_equal Date.parse('2012-07-12'), issue.start_date
620 620 assert_nil issue.due_date
621 621
622 622 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
623 623 'due_date' => '2012-07-16',
624 624 'tracker_id' => 2}, user
625 625 assert_equal Date.parse('2012-07-12'), issue.start_date
626 626 assert_equal Date.parse('2012-07-16'), issue.due_date
627 627 end
628 628
629 629 def test_safe_attributes_should_accept_target_status_writable_fields
630 630 WorkflowPermission.delete_all
631 631 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
632 632 :role_id => 1, :field_name => 'due_date',
633 633 :rule => 'readonly')
634 634 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
635 635 :role_id => 1, :field_name => 'start_date',
636 636 :rule => 'readonly')
637 637 user = User.find(2)
638 638
639 639 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
640 640
641 641 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
642 642 'due_date' => '2012-07-14'},
643 643 user
644 644 assert_equal Date.parse('2012-07-12'), issue.start_date
645 645 assert_nil issue.due_date
646 646
647 647 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
648 648 'due_date' => '2012-07-16',
649 649 'status_id' => 2},
650 650 user
651 651 assert_equal Date.parse('2012-07-12'), issue.start_date
652 652 assert_equal Date.parse('2012-07-16'), issue.due_date
653 653 end
654 654
655 655 def test_required_attributes_should_be_validated
656 656 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
657 657 :is_for_all => true, :tracker_ids => [1, 2])
658 658
659 659 WorkflowPermission.delete_all
660 660 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
661 661 :role_id => 1, :field_name => 'due_date',
662 662 :rule => 'required')
663 663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
664 664 :role_id => 1, :field_name => 'category_id',
665 665 :rule => 'required')
666 666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
667 667 :role_id => 1, :field_name => cf.id.to_s,
668 668 :rule => 'required')
669 669
670 670 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
671 671 :role_id => 1, :field_name => 'start_date',
672 672 :rule => 'required')
673 673 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
674 674 :role_id => 1, :field_name => cf.id.to_s,
675 675 :rule => 'required')
676 676 user = User.find(2)
677 677
678 678 issue = Issue.new(:project_id => 1, :tracker_id => 1,
679 679 :status_id => 1, :subject => 'Required fields',
680 680 :author => user)
681 681 assert_equal [cf.id.to_s, "category_id", "due_date"],
682 682 issue.required_attribute_names(user).sort
683 683 assert !issue.save, "Issue was saved"
684 684 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
685 685 issue.errors.full_messages.sort
686 686
687 687 issue.tracker_id = 2
688 688 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
689 689 assert !issue.save, "Issue was saved"
690 690 assert_equal ["Foo can't be blank", "Start date can't be blank"],
691 691 issue.errors.full_messages.sort
692 692
693 693 issue.start_date = Date.today
694 694 issue.custom_field_values = {cf.id.to_s => 'bar'}
695 695 assert issue.save
696 696 end
697 697
698 698 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
699 699 WorkflowPermission.delete_all
700 700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
701 701 :role_id => 1, :field_name => 'due_date',
702 702 :rule => 'required')
703 703 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
704 704 :role_id => 1, :field_name => 'start_date',
705 705 :rule => 'required')
706 706 user = User.find(2)
707 707 member = Member.find(1)
708 708 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
709 709
710 710 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
711 711
712 712 member.role_ids = [1, 2]
713 713 member.save!
714 714 assert_equal [], issue.required_attribute_names(user.reload)
715 715
716 716 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
717 717 :role_id => 2, :field_name => 'due_date',
718 718 :rule => 'required')
719 719 assert_equal %w(due_date), issue.required_attribute_names(user)
720 720
721 721 member.role_ids = [1, 2, 3]
722 722 member.save!
723 723 assert_equal [], issue.required_attribute_names(user.reload)
724 724
725 725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
726 726 :role_id => 2, :field_name => 'due_date',
727 727 :rule => 'readonly')
728 728 # required + readonly => required
729 729 assert_equal %w(due_date), issue.required_attribute_names(user)
730 730 end
731 731
732 732 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
733 733 WorkflowPermission.delete_all
734 734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
735 735 :role_id => 1, :field_name => 'due_date',
736 736 :rule => 'readonly')
737 737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
738 738 :role_id => 1, :field_name => 'start_date',
739 739 :rule => 'readonly')
740 740 user = User.find(2)
741 741 member = Member.find(1)
742 742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
743 743
744 744 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
745 745
746 746 member.role_ids = [1, 2]
747 747 member.save!
748 748 assert_equal [], issue.read_only_attribute_names(user.reload)
749 749
750 750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 751 :role_id => 2, :field_name => 'due_date',
752 752 :rule => 'readonly')
753 753 assert_equal %w(due_date), issue.read_only_attribute_names(user)
754 754 end
755 755
756 756 def test_copy
757 757 issue = Issue.new.copy_from(1)
758 758 assert issue.copy?
759 759 assert issue.save
760 760 issue.reload
761 761 orig = Issue.find(1)
762 762 assert_equal orig.subject, issue.subject
763 763 assert_equal orig.tracker, issue.tracker
764 764 assert_equal "125", issue.custom_value_for(2).value
765 765 end
766 766
767 767 def test_copy_should_copy_status
768 768 orig = Issue.find(8)
769 769 assert orig.status != IssueStatus.default
770 770
771 771 issue = Issue.new.copy_from(orig)
772 772 assert issue.save
773 773 issue.reload
774 774 assert_equal orig.status, issue.status
775 775 end
776 776
777 777 def test_copy_should_add_relation_with_copied_issue
778 778 copied = Issue.find(1)
779 779 issue = Issue.new.copy_from(copied)
780 780 assert issue.save
781 781 issue.reload
782 782
783 783 assert_equal 1, issue.relations.size
784 784 relation = issue.relations.first
785 785 assert_equal 'copied_to', relation.relation_type
786 786 assert_equal copied, relation.issue_from
787 787 assert_equal issue, relation.issue_to
788 788 end
789 789
790 790 def test_copy_should_copy_subtasks
791 791 issue = Issue.generate_with_descendants!
792 792
793 793 copy = issue.reload.copy
794 794 copy.author = User.find(7)
795 795 assert_difference 'Issue.count', 1+issue.descendants.count do
796 796 assert copy.save
797 797 end
798 798 copy.reload
799 799 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
800 800 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
801 801 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
802 802 assert_equal copy.author, child_copy.author
803 803 end
804 804
805 805 def test_copy_should_copy_subtasks_to_target_project
806 806 issue = Issue.generate_with_descendants!
807 807
808 808 copy = issue.copy(:project_id => 3)
809 809 assert_difference 'Issue.count', 1+issue.descendants.count do
810 810 assert copy.save
811 811 end
812 812 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
813 813 end
814 814
815 815 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
816 816 issue = Issue.generate_with_descendants!
817 817
818 818 copy = issue.reload.copy
819 819 assert_difference 'Issue.count', 1+issue.descendants.count do
820 820 assert copy.save
821 821 assert copy.save
822 822 end
823 823 end
824 824
825 825 def test_should_not_call_after_project_change_on_creation
826 826 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
827 827 :subject => 'Test', :author_id => 1)
828 828 issue.expects(:after_project_change).never
829 829 issue.save!
830 830 end
831 831
832 832 def test_should_not_call_after_project_change_on_update
833 833 issue = Issue.find(1)
834 834 issue.project = Project.find(1)
835 835 issue.subject = 'No project change'
836 836 issue.expects(:after_project_change).never
837 837 issue.save!
838 838 end
839 839
840 840 def test_should_call_after_project_change_on_project_change
841 841 issue = Issue.find(1)
842 842 issue.project = Project.find(2)
843 843 issue.expects(:after_project_change).once
844 844 issue.save!
845 845 end
846 846
847 847 def test_adding_journal_should_update_timestamp
848 848 issue = Issue.find(1)
849 849 updated_on_was = issue.updated_on
850 850
851 851 issue.init_journal(User.first, "Adding notes")
852 852 assert_difference 'Journal.count' do
853 853 assert issue.save
854 854 end
855 855 issue.reload
856 856
857 857 assert_not_equal updated_on_was, issue.updated_on
858 858 end
859 859
860 860 def test_should_close_duplicates
861 861 # Create 3 issues
862 862 issue1 = Issue.generate!
863 863 issue2 = Issue.generate!
864 864 issue3 = Issue.generate!
865 865
866 866 # 2 is a dupe of 1
867 867 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
868 868 :relation_type => IssueRelation::TYPE_DUPLICATES)
869 869 # And 3 is a dupe of 2
870 870 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
871 871 :relation_type => IssueRelation::TYPE_DUPLICATES)
872 872 # And 3 is a dupe of 1 (circular duplicates)
873 873 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
874 874 :relation_type => IssueRelation::TYPE_DUPLICATES)
875 875
876 876 assert issue1.reload.duplicates.include?(issue2)
877 877
878 878 # Closing issue 1
879 879 issue1.init_journal(User.first, "Closing issue1")
880 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
880 issue1.status = IssueStatus.where(:is_closed => true).first
881 881 assert issue1.save
882 882 # 2 and 3 should be also closed
883 883 assert issue2.reload.closed?
884 884 assert issue3.reload.closed?
885 885 end
886 886
887 887 def test_should_not_close_duplicated_issue
888 888 issue1 = Issue.generate!
889 889 issue2 = Issue.generate!
890 890
891 891 # 2 is a dupe of 1
892 892 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
893 893 :relation_type => IssueRelation::TYPE_DUPLICATES)
894 894 # 2 is a dup of 1 but 1 is not a duplicate of 2
895 895 assert !issue2.reload.duplicates.include?(issue1)
896 896
897 897 # Closing issue 2
898 898 issue2.init_journal(User.first, "Closing issue2")
899 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
899 issue2.status = IssueStatus.where(:is_closed => true).first
900 900 assert issue2.save
901 901 # 1 should not be also closed
902 902 assert !issue1.reload.closed?
903 903 end
904 904
905 905 def test_assignable_versions
906 906 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
907 907 :status_id => 1, :fixed_version_id => 1,
908 908 :subject => 'New issue')
909 909 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
910 910 end
911 911
912 912 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
913 913 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
914 914 :status_id => 1, :fixed_version_id => 1,
915 915 :subject => 'New issue')
916 916 assert !issue.save
917 917 assert_not_nil issue.errors[:fixed_version_id]
918 918 end
919 919
920 920 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
921 921 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
922 922 :status_id => 1, :fixed_version_id => 2,
923 923 :subject => 'New issue')
924 924 assert !issue.save
925 925 assert_not_nil issue.errors[:fixed_version_id]
926 926 end
927 927
928 928 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
929 929 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
930 930 :status_id => 1, :fixed_version_id => 3,
931 931 :subject => 'New issue')
932 932 assert issue.save
933 933 end
934 934
935 935 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
936 936 issue = Issue.find(11)
937 937 assert_equal 'closed', issue.fixed_version.status
938 938 issue.subject = 'Subject changed'
939 939 assert issue.save
940 940 end
941 941
942 942 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
943 943 issue = Issue.find(11)
944 944 issue.status_id = 1
945 945 assert !issue.save
946 946 assert_not_nil issue.errors[:base]
947 947 end
948 948
949 949 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
950 950 issue = Issue.find(11)
951 951 issue.status_id = 1
952 952 issue.fixed_version_id = 3
953 953 assert issue.save
954 954 end
955 955
956 956 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
957 957 issue = Issue.find(12)
958 958 assert_equal 'locked', issue.fixed_version.status
959 959 issue.status_id = 1
960 960 assert issue.save
961 961 end
962 962
963 963 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
964 964 issue = Issue.find(2)
965 965 assert_equal 2, issue.fixed_version_id
966 966 issue.project_id = 3
967 967 assert_nil issue.fixed_version_id
968 968 issue.fixed_version_id = 2
969 969 assert !issue.save
970 970 assert_include 'Target version is not included in the list', issue.errors.full_messages
971 971 end
972 972
973 973 def test_should_keep_shared_version_when_changing_project
974 974 Version.find(2).update_attribute :sharing, 'tree'
975 975
976 976 issue = Issue.find(2)
977 977 assert_equal 2, issue.fixed_version_id
978 978 issue.project_id = 3
979 979 assert_equal 2, issue.fixed_version_id
980 980 assert issue.save
981 981 end
982 982
983 983 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
984 984 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
985 985 end
986 986
987 987 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
988 988 Project.find(2).disable_module! :issue_tracking
989 989 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
990 990 end
991 991
992 992 def test_move_to_another_project_with_same_category
993 993 issue = Issue.find(1)
994 994 issue.project = Project.find(2)
995 995 assert issue.save
996 996 issue.reload
997 997 assert_equal 2, issue.project_id
998 998 # Category changes
999 999 assert_equal 4, issue.category_id
1000 1000 # Make sure time entries were move to the target project
1001 1001 assert_equal 2, issue.time_entries.first.project_id
1002 1002 end
1003 1003
1004 1004 def test_move_to_another_project_without_same_category
1005 1005 issue = Issue.find(2)
1006 1006 issue.project = Project.find(2)
1007 1007 assert issue.save
1008 1008 issue.reload
1009 1009 assert_equal 2, issue.project_id
1010 1010 # Category cleared
1011 1011 assert_nil issue.category_id
1012 1012 end
1013 1013
1014 1014 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1015 1015 issue = Issue.find(1)
1016 1016 issue.update_attribute(:fixed_version_id, 1)
1017 1017 issue.project = Project.find(2)
1018 1018 assert issue.save
1019 1019 issue.reload
1020 1020 assert_equal 2, issue.project_id
1021 1021 # Cleared fixed_version
1022 1022 assert_equal nil, issue.fixed_version
1023 1023 end
1024 1024
1025 1025 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1026 1026 issue = Issue.find(1)
1027 1027 issue.update_attribute(:fixed_version_id, 4)
1028 1028 issue.project = Project.find(5)
1029 1029 assert issue.save
1030 1030 issue.reload
1031 1031 assert_equal 5, issue.project_id
1032 1032 # Keep fixed_version
1033 1033 assert_equal 4, issue.fixed_version_id
1034 1034 end
1035 1035
1036 1036 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1037 1037 issue = Issue.find(1)
1038 1038 issue.update_attribute(:fixed_version_id, 1)
1039 1039 issue.project = Project.find(5)
1040 1040 assert issue.save
1041 1041 issue.reload
1042 1042 assert_equal 5, issue.project_id
1043 1043 # Cleared fixed_version
1044 1044 assert_equal nil, issue.fixed_version
1045 1045 end
1046 1046
1047 1047 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1048 1048 issue = Issue.find(1)
1049 1049 issue.update_attribute(:fixed_version_id, 7)
1050 1050 issue.project = Project.find(2)
1051 1051 assert issue.save
1052 1052 issue.reload
1053 1053 assert_equal 2, issue.project_id
1054 1054 # Keep fixed_version
1055 1055 assert_equal 7, issue.fixed_version_id
1056 1056 end
1057 1057
1058 1058 def test_move_to_another_project_should_keep_parent_if_valid
1059 1059 issue = Issue.find(1)
1060 1060 issue.update_attribute(:parent_issue_id, 2)
1061 1061 issue.project = Project.find(3)
1062 1062 assert issue.save
1063 1063 issue.reload
1064 1064 assert_equal 2, issue.parent_id
1065 1065 end
1066 1066
1067 1067 def test_move_to_another_project_should_clear_parent_if_not_valid
1068 1068 issue = Issue.find(1)
1069 1069 issue.update_attribute(:parent_issue_id, 2)
1070 1070 issue.project = Project.find(2)
1071 1071 assert issue.save
1072 1072 issue.reload
1073 1073 assert_nil issue.parent_id
1074 1074 end
1075 1075
1076 1076 def test_move_to_another_project_with_disabled_tracker
1077 1077 issue = Issue.find(1)
1078 1078 target = Project.find(2)
1079 1079 target.tracker_ids = [3]
1080 1080 target.save
1081 1081 issue.project = target
1082 1082 assert issue.save
1083 1083 issue.reload
1084 1084 assert_equal 2, issue.project_id
1085 1085 assert_equal 3, issue.tracker_id
1086 1086 end
1087 1087
1088 1088 def test_copy_to_the_same_project
1089 1089 issue = Issue.find(1)
1090 1090 copy = issue.copy
1091 1091 assert_difference 'Issue.count' do
1092 1092 copy.save!
1093 1093 end
1094 1094 assert_kind_of Issue, copy
1095 1095 assert_equal issue.project, copy.project
1096 1096 assert_equal "125", copy.custom_value_for(2).value
1097 1097 end
1098 1098
1099 1099 def test_copy_to_another_project_and_tracker
1100 1100 issue = Issue.find(1)
1101 1101 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1102 1102 assert_difference 'Issue.count' do
1103 1103 copy.save!
1104 1104 end
1105 1105 copy.reload
1106 1106 assert_kind_of Issue, copy
1107 1107 assert_equal Project.find(3), copy.project
1108 1108 assert_equal Tracker.find(2), copy.tracker
1109 1109 # Custom field #2 is not associated with target tracker
1110 1110 assert_nil copy.custom_value_for(2)
1111 1111 end
1112 1112
1113 1113 context "#copy" do
1114 1114 setup do
1115 1115 @issue = Issue.find(1)
1116 1116 end
1117 1117
1118 1118 should "not create a journal" do
1119 1119 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1120 1120 copy.save!
1121 1121 assert_equal 0, copy.reload.journals.size
1122 1122 end
1123 1123
1124 1124 should "allow assigned_to changes" do
1125 1125 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1126 1126 assert_equal 3, copy.assigned_to_id
1127 1127 end
1128 1128
1129 1129 should "allow status changes" do
1130 1130 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1131 1131 assert_equal 2, copy.status_id
1132 1132 end
1133 1133
1134 1134 should "allow start date changes" do
1135 1135 date = Date.today
1136 1136 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1137 1137 assert_equal date, copy.start_date
1138 1138 end
1139 1139
1140 1140 should "allow due date changes" do
1141 1141 date = Date.today
1142 1142 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1143 1143 assert_equal date, copy.due_date
1144 1144 end
1145 1145
1146 1146 should "set current user as author" do
1147 1147 User.current = User.find(9)
1148 1148 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
1149 1149 assert_equal User.current, copy.author
1150 1150 end
1151 1151
1152 1152 should "create a journal with notes" do
1153 1153 date = Date.today
1154 1154 notes = "Notes added when copying"
1155 1155 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1156 1156 copy.init_journal(User.current, notes)
1157 1157 copy.save!
1158 1158
1159 1159 assert_equal 1, copy.journals.size
1160 1160 journal = copy.journals.first
1161 1161 assert_equal 0, journal.details.size
1162 1162 assert_equal notes, journal.notes
1163 1163 end
1164 1164 end
1165 1165
1166 1166 def test_valid_parent_project
1167 1167 issue = Issue.find(1)
1168 1168 issue_in_same_project = Issue.find(2)
1169 1169 issue_in_child_project = Issue.find(5)
1170 1170 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1171 1171 issue_in_other_child_project = Issue.find(6)
1172 1172 issue_in_different_tree = Issue.find(4)
1173 1173
1174 1174 with_settings :cross_project_subtasks => '' do
1175 1175 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1176 1176 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1177 1177 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1178 1178 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1179 1179 end
1180 1180
1181 1181 with_settings :cross_project_subtasks => 'system' do
1182 1182 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1183 1183 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1184 1184 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1185 1185 end
1186 1186
1187 1187 with_settings :cross_project_subtasks => 'tree' do
1188 1188 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1189 1189 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1190 1190 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1191 1191 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1192 1192
1193 1193 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1194 1194 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1195 1195 end
1196 1196
1197 1197 with_settings :cross_project_subtasks => 'descendants' do
1198 1198 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1199 1199 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1200 1200 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1201 1201 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1202 1202
1203 1203 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1204 1204 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1205 1205 end
1206 1206 end
1207 1207
1208 1208 def test_recipients_should_include_previous_assignee
1209 1209 user = User.find(3)
1210 1210 user.members.update_all ["mail_notification = ?", false]
1211 1211 user.update_attribute :mail_notification, 'only_assigned'
1212 1212
1213 1213 issue = Issue.find(2)
1214 1214 issue.assigned_to = nil
1215 1215 assert_include user.mail, issue.recipients
1216 1216 issue.save!
1217 1217 assert !issue.recipients.include?(user.mail)
1218 1218 end
1219 1219
1220 1220 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1221 1221 issue = Issue.find(12)
1222 1222 assert issue.recipients.include?(issue.author.mail)
1223 1223 # copy the issue to a private project
1224 1224 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1225 1225 # author is not a member of project anymore
1226 1226 assert !copy.recipients.include?(copy.author.mail)
1227 1227 end
1228 1228
1229 1229 def test_recipients_should_include_the_assigned_group_members
1230 1230 group_member = User.generate!
1231 1231 group = Group.generate!
1232 1232 group.users << group_member
1233 1233
1234 1234 issue = Issue.find(12)
1235 1235 issue.assigned_to = group
1236 1236 assert issue.recipients.include?(group_member.mail)
1237 1237 end
1238 1238
1239 1239 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1240 1240 user = User.find(3)
1241 1241 issue = Issue.find(9)
1242 1242 Watcher.create!(:user => user, :watchable => issue)
1243 1243 assert issue.watched_by?(user)
1244 1244 assert !issue.watcher_recipients.include?(user.mail)
1245 1245 end
1246 1246
1247 1247 def test_issue_destroy
1248 1248 Issue.find(1).destroy
1249 1249 assert_nil Issue.find_by_id(1)
1250 1250 assert_nil TimeEntry.find_by_issue_id(1)
1251 1251 end
1252 1252
1253 1253 def test_destroying_a_deleted_issue_should_not_raise_an_error
1254 1254 issue = Issue.find(1)
1255 1255 Issue.find(1).destroy
1256 1256
1257 1257 assert_nothing_raised do
1258 1258 assert_no_difference 'Issue.count' do
1259 1259 issue.destroy
1260 1260 end
1261 1261 assert issue.destroyed?
1262 1262 end
1263 1263 end
1264 1264
1265 1265 def test_destroying_a_stale_issue_should_not_raise_an_error
1266 1266 issue = Issue.find(1)
1267 1267 Issue.find(1).update_attribute :subject, "Updated"
1268 1268
1269 1269 assert_nothing_raised do
1270 1270 assert_difference 'Issue.count', -1 do
1271 1271 issue.destroy
1272 1272 end
1273 1273 assert issue.destroyed?
1274 1274 end
1275 1275 end
1276 1276
1277 1277 def test_blocked
1278 1278 blocked_issue = Issue.find(9)
1279 1279 blocking_issue = Issue.find(10)
1280 1280
1281 1281 assert blocked_issue.blocked?
1282 1282 assert !blocking_issue.blocked?
1283 1283 end
1284 1284
1285 1285 def test_blocked_issues_dont_allow_closed_statuses
1286 1286 blocked_issue = Issue.find(9)
1287 1287
1288 1288 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1289 1289 assert !allowed_statuses.empty?
1290 1290 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1291 1291 assert closed_statuses.empty?
1292 1292 end
1293 1293
1294 1294 def test_unblocked_issues_allow_closed_statuses
1295 1295 blocking_issue = Issue.find(10)
1296 1296
1297 1297 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1298 1298 assert !allowed_statuses.empty?
1299 1299 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1300 1300 assert !closed_statuses.empty?
1301 1301 end
1302 1302
1303 1303 def test_reschedule_an_issue_without_dates
1304 1304 with_settings :non_working_week_days => [] do
1305 1305 issue = Issue.new(:start_date => nil, :due_date => nil)
1306 1306 issue.reschedule_on '2012-10-09'.to_date
1307 1307 assert_equal '2012-10-09'.to_date, issue.start_date
1308 1308 assert_equal '2012-10-09'.to_date, issue.due_date
1309 1309 end
1310 1310
1311 1311 with_settings :non_working_week_days => %w(6 7) do
1312 1312 issue = Issue.new(:start_date => nil, :due_date => nil)
1313 1313 issue.reschedule_on '2012-10-09'.to_date
1314 1314 assert_equal '2012-10-09'.to_date, issue.start_date
1315 1315 assert_equal '2012-10-09'.to_date, issue.due_date
1316 1316
1317 1317 issue = Issue.new(:start_date => nil, :due_date => nil)
1318 1318 issue.reschedule_on '2012-10-13'.to_date
1319 1319 assert_equal '2012-10-15'.to_date, issue.start_date
1320 1320 assert_equal '2012-10-15'.to_date, issue.due_date
1321 1321 end
1322 1322 end
1323 1323
1324 1324 def test_reschedule_an_issue_with_start_date
1325 1325 with_settings :non_working_week_days => [] do
1326 1326 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1327 1327 issue.reschedule_on '2012-10-13'.to_date
1328 1328 assert_equal '2012-10-13'.to_date, issue.start_date
1329 1329 assert_equal '2012-10-13'.to_date, issue.due_date
1330 1330 end
1331 1331
1332 1332 with_settings :non_working_week_days => %w(6 7) do
1333 1333 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1334 1334 issue.reschedule_on '2012-10-11'.to_date
1335 1335 assert_equal '2012-10-11'.to_date, issue.start_date
1336 1336 assert_equal '2012-10-11'.to_date, issue.due_date
1337 1337
1338 1338 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1339 1339 issue.reschedule_on '2012-10-13'.to_date
1340 1340 assert_equal '2012-10-15'.to_date, issue.start_date
1341 1341 assert_equal '2012-10-15'.to_date, issue.due_date
1342 1342 end
1343 1343 end
1344 1344
1345 1345 def test_reschedule_an_issue_with_start_and_due_dates
1346 1346 with_settings :non_working_week_days => [] do
1347 1347 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1348 1348 issue.reschedule_on '2012-10-13'.to_date
1349 1349 assert_equal '2012-10-13'.to_date, issue.start_date
1350 1350 assert_equal '2012-10-19'.to_date, issue.due_date
1351 1351 end
1352 1352
1353 1353 with_settings :non_working_week_days => %w(6 7) do
1354 1354 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1355 1355 issue.reschedule_on '2012-10-11'.to_date
1356 1356 assert_equal '2012-10-11'.to_date, issue.start_date
1357 1357 assert_equal '2012-10-23'.to_date, issue.due_date
1358 1358
1359 1359 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1360 1360 issue.reschedule_on '2012-10-13'.to_date
1361 1361 assert_equal '2012-10-15'.to_date, issue.start_date
1362 1362 assert_equal '2012-10-25'.to_date, issue.due_date
1363 1363 end
1364 1364 end
1365 1365
1366 1366 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1367 1367 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1368 1368 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1369 1369 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1370 1370 :relation_type => IssueRelation::TYPE_PRECEDES)
1371 1371 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1372 1372
1373 1373 issue1.due_date = '2012-10-23'
1374 1374 issue1.save!
1375 1375 issue2.reload
1376 1376 assert_equal Date.parse('2012-10-24'), issue2.start_date
1377 1377 assert_equal Date.parse('2012-10-26'), issue2.due_date
1378 1378 end
1379 1379
1380 1380 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1381 1381 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1382 1382 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1383 1383 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1384 1384 :relation_type => IssueRelation::TYPE_PRECEDES)
1385 1385 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1386 1386
1387 1387 issue1.start_date = '2012-09-17'
1388 1388 issue1.due_date = '2012-09-18'
1389 1389 issue1.save!
1390 1390 issue2.reload
1391 1391 assert_equal Date.parse('2012-09-19'), issue2.start_date
1392 1392 assert_equal Date.parse('2012-09-21'), issue2.due_date
1393 1393 end
1394 1394
1395 1395 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1396 1396 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1397 1397 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1398 1398 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1399 1399 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1400 1400 :relation_type => IssueRelation::TYPE_PRECEDES)
1401 1401 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1402 1402 :relation_type => IssueRelation::TYPE_PRECEDES)
1403 1403 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1404 1404
1405 1405 issue1.start_date = '2012-09-17'
1406 1406 issue1.due_date = '2012-09-18'
1407 1407 issue1.save!
1408 1408 issue2.reload
1409 1409 # Issue 2 must start after Issue 3
1410 1410 assert_equal Date.parse('2012-10-03'), issue2.start_date
1411 1411 assert_equal Date.parse('2012-10-05'), issue2.due_date
1412 1412 end
1413 1413
1414 1414 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1415 1415 with_settings :non_working_week_days => [] do
1416 1416 stale = Issue.find(1)
1417 1417 issue = Issue.find(1)
1418 1418 issue.subject = "Updated"
1419 1419 issue.save!
1420 1420 date = 10.days.from_now.to_date
1421 1421 assert_nothing_raised do
1422 1422 stale.reschedule_on!(date)
1423 1423 end
1424 1424 assert_equal date, stale.reload.start_date
1425 1425 end
1426 1426 end
1427 1427
1428 1428 def test_overdue
1429 1429 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1430 1430 assert !Issue.new(:due_date => Date.today).overdue?
1431 1431 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1432 1432 assert !Issue.new(:due_date => nil).overdue?
1433 1433 assert !Issue.new(:due_date => 1.day.ago.to_date,
1434 1434 :status => IssueStatus.where(:is_closed => true).first
1435 1435 ).overdue?
1436 1436 end
1437 1437
1438 1438 context "#behind_schedule?" do
1439 1439 should "be false if the issue has no start_date" do
1440 1440 assert !Issue.new(:start_date => nil,
1441 1441 :due_date => 1.day.from_now.to_date,
1442 1442 :done_ratio => 0).behind_schedule?
1443 1443 end
1444 1444
1445 1445 should "be false if the issue has no end_date" do
1446 1446 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1447 1447 :due_date => nil,
1448 1448 :done_ratio => 0).behind_schedule?
1449 1449 end
1450 1450
1451 1451 should "be false if the issue has more done than it's calendar time" do
1452 1452 assert !Issue.new(:start_date => 50.days.ago.to_date,
1453 1453 :due_date => 50.days.from_now.to_date,
1454 1454 :done_ratio => 90).behind_schedule?
1455 1455 end
1456 1456
1457 1457 should "be true if the issue hasn't been started at all" do
1458 1458 assert Issue.new(:start_date => 1.day.ago.to_date,
1459 1459 :due_date => 1.day.from_now.to_date,
1460 1460 :done_ratio => 0).behind_schedule?
1461 1461 end
1462 1462
1463 1463 should "be true if the issue has used more calendar time than it's done ratio" do
1464 1464 assert Issue.new(:start_date => 100.days.ago.to_date,
1465 1465 :due_date => Date.today,
1466 1466 :done_ratio => 90).behind_schedule?
1467 1467 end
1468 1468 end
1469 1469
1470 1470 context "#assignable_users" do
1471 1471 should "be Users" do
1472 1472 assert_kind_of User, Issue.find(1).assignable_users.first
1473 1473 end
1474 1474
1475 1475 should "include the issue author" do
1476 1476 non_project_member = User.generate!
1477 1477 issue = Issue.generate!(:author => non_project_member)
1478 1478
1479 1479 assert issue.assignable_users.include?(non_project_member)
1480 1480 end
1481 1481
1482 1482 should "include the current assignee" do
1483 1483 user = User.generate!
1484 1484 issue = Issue.generate!(:assigned_to => user)
1485 1485 user.lock!
1486 1486
1487 1487 assert Issue.find(issue.id).assignable_users.include?(user)
1488 1488 end
1489 1489
1490 1490 should "not show the issue author twice" do
1491 1491 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1492 1492 assert_equal 2, assignable_user_ids.length
1493 1493
1494 1494 assignable_user_ids.each do |user_id|
1495 1495 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1496 1496 "User #{user_id} appears more or less than once"
1497 1497 end
1498 1498 end
1499 1499
1500 1500 context "with issue_group_assignment" do
1501 1501 should "include groups" do
1502 1502 issue = Issue.new(:project => Project.find(2))
1503 1503
1504 1504 with_settings :issue_group_assignment => '1' do
1505 1505 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1506 1506 assert issue.assignable_users.include?(Group.find(11))
1507 1507 end
1508 1508 end
1509 1509 end
1510 1510
1511 1511 context "without issue_group_assignment" do
1512 1512 should "not include groups" do
1513 1513 issue = Issue.new(:project => Project.find(2))
1514 1514
1515 1515 with_settings :issue_group_assignment => '0' do
1516 1516 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1517 1517 assert !issue.assignable_users.include?(Group.find(11))
1518 1518 end
1519 1519 end
1520 1520 end
1521 1521 end
1522 1522
1523 1523 def test_create_should_send_email_notification
1524 1524 ActionMailer::Base.deliveries.clear
1525 1525 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1526 1526 :author_id => 3, :status_id => 1,
1527 1527 :priority => IssuePriority.all.first,
1528 1528 :subject => 'test_create', :estimated_hours => '1:30')
1529 1529
1530 1530 assert issue.save
1531 1531 assert_equal 1, ActionMailer::Base.deliveries.size
1532 1532 end
1533 1533
1534 1534 def test_stale_issue_should_not_send_email_notification
1535 1535 ActionMailer::Base.deliveries.clear
1536 1536 issue = Issue.find(1)
1537 1537 stale = Issue.find(1)
1538 1538
1539 1539 issue.init_journal(User.find(1))
1540 1540 issue.subject = 'Subjet update'
1541 1541 assert issue.save
1542 1542 assert_equal 1, ActionMailer::Base.deliveries.size
1543 1543 ActionMailer::Base.deliveries.clear
1544 1544
1545 1545 stale.init_journal(User.find(1))
1546 1546 stale.subject = 'Another subjet update'
1547 1547 assert_raise ActiveRecord::StaleObjectError do
1548 1548 stale.save
1549 1549 end
1550 1550 assert ActionMailer::Base.deliveries.empty?
1551 1551 end
1552 1552
1553 1553 def test_journalized_description
1554 1554 IssueCustomField.delete_all
1555 1555
1556 1556 i = Issue.first
1557 1557 old_description = i.description
1558 1558 new_description = "This is the new description"
1559 1559
1560 1560 i.init_journal(User.find(2))
1561 1561 i.description = new_description
1562 1562 assert_difference 'Journal.count', 1 do
1563 1563 assert_difference 'JournalDetail.count', 1 do
1564 1564 i.save!
1565 1565 end
1566 1566 end
1567 1567
1568 1568 detail = JournalDetail.first(:order => 'id DESC')
1569 1569 assert_equal i, detail.journal.journalized
1570 1570 assert_equal 'attr', detail.property
1571 1571 assert_equal 'description', detail.prop_key
1572 1572 assert_equal old_description, detail.old_value
1573 1573 assert_equal new_description, detail.value
1574 1574 end
1575 1575
1576 1576 def test_blank_descriptions_should_not_be_journalized
1577 1577 IssueCustomField.delete_all
1578 1578 Issue.update_all("description = NULL", "id=1")
1579 1579
1580 1580 i = Issue.find(1)
1581 1581 i.init_journal(User.find(2))
1582 1582 i.subject = "blank description"
1583 1583 i.description = "\r\n"
1584 1584
1585 1585 assert_difference 'Journal.count', 1 do
1586 1586 assert_difference 'JournalDetail.count', 1 do
1587 1587 i.save!
1588 1588 end
1589 1589 end
1590 1590 end
1591 1591
1592 1592 def test_journalized_multi_custom_field
1593 1593 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1594 1594 :is_filter => true, :is_for_all => true,
1595 1595 :tracker_ids => [1],
1596 1596 :possible_values => ['value1', 'value2', 'value3'],
1597 1597 :multiple => true)
1598 1598
1599 1599 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1600 1600 :subject => 'Test', :author_id => 1)
1601 1601
1602 1602 assert_difference 'Journal.count' do
1603 1603 assert_difference 'JournalDetail.count' do
1604 1604 issue.init_journal(User.first)
1605 1605 issue.custom_field_values = {field.id => ['value1']}
1606 1606 issue.save!
1607 1607 end
1608 1608 assert_difference 'JournalDetail.count' do
1609 1609 issue.init_journal(User.first)
1610 1610 issue.custom_field_values = {field.id => ['value1', 'value2']}
1611 1611 issue.save!
1612 1612 end
1613 1613 assert_difference 'JournalDetail.count', 2 do
1614 1614 issue.init_journal(User.first)
1615 1615 issue.custom_field_values = {field.id => ['value3', 'value2']}
1616 1616 issue.save!
1617 1617 end
1618 1618 assert_difference 'JournalDetail.count', 2 do
1619 1619 issue.init_journal(User.first)
1620 1620 issue.custom_field_values = {field.id => nil}
1621 1621 issue.save!
1622 1622 end
1623 1623 end
1624 1624 end
1625 1625
1626 1626 def test_description_eol_should_be_normalized
1627 1627 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1628 1628 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1629 1629 end
1630 1630
1631 1631 def test_saving_twice_should_not_duplicate_journal_details
1632 1632 i = Issue.first
1633 1633 i.init_journal(User.find(2), 'Some notes')
1634 1634 # initial changes
1635 1635 i.subject = 'New subject'
1636 1636 i.done_ratio = i.done_ratio + 10
1637 1637 assert_difference 'Journal.count' do
1638 1638 assert i.save
1639 1639 end
1640 1640 # 1 more change
1641 1641 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1642 1642 assert_no_difference 'Journal.count' do
1643 1643 assert_difference 'JournalDetail.count', 1 do
1644 1644 i.save
1645 1645 end
1646 1646 end
1647 1647 # no more change
1648 1648 assert_no_difference 'Journal.count' do
1649 1649 assert_no_difference 'JournalDetail.count' do
1650 1650 i.save
1651 1651 end
1652 1652 end
1653 1653 end
1654 1654
1655 1655 def test_all_dependent_issues
1656 1656 IssueRelation.delete_all
1657 1657 assert IssueRelation.create!(:issue_from => Issue.find(1),
1658 1658 :issue_to => Issue.find(2),
1659 1659 :relation_type => IssueRelation::TYPE_PRECEDES)
1660 1660 assert IssueRelation.create!(:issue_from => Issue.find(2),
1661 1661 :issue_to => Issue.find(3),
1662 1662 :relation_type => IssueRelation::TYPE_PRECEDES)
1663 1663 assert IssueRelation.create!(:issue_from => Issue.find(3),
1664 1664 :issue_to => Issue.find(8),
1665 1665 :relation_type => IssueRelation::TYPE_PRECEDES)
1666 1666
1667 1667 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1668 1668 end
1669 1669
1670 1670 def test_all_dependent_issues_with_persistent_circular_dependency
1671 1671 IssueRelation.delete_all
1672 1672 assert IssueRelation.create!(:issue_from => Issue.find(1),
1673 1673 :issue_to => Issue.find(2),
1674 1674 :relation_type => IssueRelation::TYPE_PRECEDES)
1675 1675 assert IssueRelation.create!(:issue_from => Issue.find(2),
1676 1676 :issue_to => Issue.find(3),
1677 1677 :relation_type => IssueRelation::TYPE_PRECEDES)
1678 1678
1679 1679 r = IssueRelation.create!(:issue_from => Issue.find(3),
1680 1680 :issue_to => Issue.find(7),
1681 1681 :relation_type => IssueRelation::TYPE_PRECEDES)
1682 1682 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1683 1683
1684 1684 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1685 1685 end
1686 1686
1687 1687 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1688 1688 IssueRelation.delete_all
1689 1689 assert IssueRelation.create!(:issue_from => Issue.find(1),
1690 1690 :issue_to => Issue.find(2),
1691 1691 :relation_type => IssueRelation::TYPE_RELATES)
1692 1692 assert IssueRelation.create!(:issue_from => Issue.find(2),
1693 1693 :issue_to => Issue.find(3),
1694 1694 :relation_type => IssueRelation::TYPE_RELATES)
1695 1695 assert IssueRelation.create!(:issue_from => Issue.find(3),
1696 1696 :issue_to => Issue.find(8),
1697 1697 :relation_type => IssueRelation::TYPE_RELATES)
1698 1698
1699 1699 r = IssueRelation.create!(:issue_from => Issue.find(8),
1700 1700 :issue_to => Issue.find(7),
1701 1701 :relation_type => IssueRelation::TYPE_RELATES)
1702 1702 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1703 1703
1704 1704 r = IssueRelation.create!(:issue_from => Issue.find(3),
1705 1705 :issue_to => Issue.find(7),
1706 1706 :relation_type => IssueRelation::TYPE_RELATES)
1707 1707 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1708 1708
1709 1709 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1710 1710 end
1711 1711
1712 1712 context "#done_ratio" do
1713 1713 setup do
1714 1714 @issue = Issue.find(1)
1715 1715 @issue_status = IssueStatus.find(1)
1716 1716 @issue_status.update_attribute(:default_done_ratio, 50)
1717 1717 @issue2 = Issue.find(2)
1718 1718 @issue_status2 = IssueStatus.find(2)
1719 1719 @issue_status2.update_attribute(:default_done_ratio, 0)
1720 1720 end
1721 1721
1722 1722 teardown do
1723 1723 Setting.issue_done_ratio = 'issue_field'
1724 1724 end
1725 1725
1726 1726 context "with Setting.issue_done_ratio using the issue_field" do
1727 1727 setup do
1728 1728 Setting.issue_done_ratio = 'issue_field'
1729 1729 end
1730 1730
1731 1731 should "read the issue's field" do
1732 1732 assert_equal 0, @issue.done_ratio
1733 1733 assert_equal 30, @issue2.done_ratio
1734 1734 end
1735 1735 end
1736 1736
1737 1737 context "with Setting.issue_done_ratio using the issue_status" do
1738 1738 setup do
1739 1739 Setting.issue_done_ratio = 'issue_status'
1740 1740 end
1741 1741
1742 1742 should "read the Issue Status's default done ratio" do
1743 1743 assert_equal 50, @issue.done_ratio
1744 1744 assert_equal 0, @issue2.done_ratio
1745 1745 end
1746 1746 end
1747 1747 end
1748 1748
1749 1749 context "#update_done_ratio_from_issue_status" do
1750 1750 setup do
1751 1751 @issue = Issue.find(1)
1752 1752 @issue_status = IssueStatus.find(1)
1753 1753 @issue_status.update_attribute(:default_done_ratio, 50)
1754 1754 @issue2 = Issue.find(2)
1755 1755 @issue_status2 = IssueStatus.find(2)
1756 1756 @issue_status2.update_attribute(:default_done_ratio, 0)
1757 1757 end
1758 1758
1759 1759 context "with Setting.issue_done_ratio using the issue_field" do
1760 1760 setup do
1761 1761 Setting.issue_done_ratio = 'issue_field'
1762 1762 end
1763 1763
1764 1764 should "not change the issue" do
1765 1765 @issue.update_done_ratio_from_issue_status
1766 1766 @issue2.update_done_ratio_from_issue_status
1767 1767
1768 1768 assert_equal 0, @issue.read_attribute(:done_ratio)
1769 1769 assert_equal 30, @issue2.read_attribute(:done_ratio)
1770 1770 end
1771 1771 end
1772 1772
1773 1773 context "with Setting.issue_done_ratio using the issue_status" do
1774 1774 setup do
1775 1775 Setting.issue_done_ratio = 'issue_status'
1776 1776 end
1777 1777
1778 1778 should "change the issue's done ratio" do
1779 1779 @issue.update_done_ratio_from_issue_status
1780 1780 @issue2.update_done_ratio_from_issue_status
1781 1781
1782 1782 assert_equal 50, @issue.read_attribute(:done_ratio)
1783 1783 assert_equal 0, @issue2.read_attribute(:done_ratio)
1784 1784 end
1785 1785 end
1786 1786 end
1787 1787
1788 1788 test "#by_tracker" do
1789 1789 User.current = User.anonymous
1790 1790 groups = Issue.by_tracker(Project.find(1))
1791 1791 assert_equal 3, groups.size
1792 1792 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1793 1793 end
1794 1794
1795 1795 test "#by_version" do
1796 1796 User.current = User.anonymous
1797 1797 groups = Issue.by_version(Project.find(1))
1798 1798 assert_equal 3, groups.size
1799 1799 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1800 1800 end
1801 1801
1802 1802 test "#by_priority" do
1803 1803 User.current = User.anonymous
1804 1804 groups = Issue.by_priority(Project.find(1))
1805 1805 assert_equal 4, groups.size
1806 1806 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1807 1807 end
1808 1808
1809 1809 test "#by_category" do
1810 1810 User.current = User.anonymous
1811 1811 groups = Issue.by_category(Project.find(1))
1812 1812 assert_equal 2, groups.size
1813 1813 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1814 1814 end
1815 1815
1816 1816 test "#by_assigned_to" do
1817 1817 User.current = User.anonymous
1818 1818 groups = Issue.by_assigned_to(Project.find(1))
1819 1819 assert_equal 2, groups.size
1820 1820 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1821 1821 end
1822 1822
1823 1823 test "#by_author" do
1824 1824 User.current = User.anonymous
1825 1825 groups = Issue.by_author(Project.find(1))
1826 1826 assert_equal 4, groups.size
1827 1827 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1828 1828 end
1829 1829
1830 1830 test "#by_subproject" do
1831 1831 User.current = User.anonymous
1832 1832 groups = Issue.by_subproject(Project.find(1))
1833 1833 # Private descendant not visible
1834 1834 assert_equal 1, groups.size
1835 1835 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1836 1836 end
1837 1837
1838 1838 def test_recently_updated_scope
1839 1839 #should return the last updated issue
1840 1840 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1841 1841 end
1842 1842
1843 1843 def test_on_active_projects_scope
1844 1844 assert Project.find(2).archive
1845 1845
1846 1846 before = Issue.on_active_project.length
1847 1847 # test inclusion to results
1848 1848 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1849 1849 assert_equal before + 1, Issue.on_active_project.length
1850 1850
1851 1851 # Move to an archived project
1852 1852 issue.project = Project.find(2)
1853 1853 assert issue.save
1854 1854 assert_equal before, Issue.on_active_project.length
1855 1855 end
1856 1856
1857 1857 context "Issue#recipients" do
1858 1858 setup do
1859 1859 @project = Project.find(1)
1860 1860 @author = User.generate!
1861 1861 @assignee = User.generate!
1862 1862 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1863 1863 end
1864 1864
1865 1865 should "include project recipients" do
1866 1866 assert @project.recipients.present?
1867 1867 @project.recipients.each do |project_recipient|
1868 1868 assert @issue.recipients.include?(project_recipient)
1869 1869 end
1870 1870 end
1871 1871
1872 1872 should "include the author if the author is active" do
1873 1873 assert @issue.author, "No author set for Issue"
1874 1874 assert @issue.recipients.include?(@issue.author.mail)
1875 1875 end
1876 1876
1877 1877 should "include the assigned to user if the assigned to user is active" do
1878 1878 assert @issue.assigned_to, "No assigned_to set for Issue"
1879 1879 assert @issue.recipients.include?(@issue.assigned_to.mail)
1880 1880 end
1881 1881
1882 1882 should "not include users who opt out of all email" do
1883 1883 @author.update_attribute(:mail_notification, :none)
1884 1884
1885 1885 assert !@issue.recipients.include?(@issue.author.mail)
1886 1886 end
1887 1887
1888 1888 should "not include the issue author if they are only notified of assigned issues" do
1889 1889 @author.update_attribute(:mail_notification, :only_assigned)
1890 1890
1891 1891 assert !@issue.recipients.include?(@issue.author.mail)
1892 1892 end
1893 1893
1894 1894 should "not include the assigned user if they are only notified of owned issues" do
1895 1895 @assignee.update_attribute(:mail_notification, :only_owner)
1896 1896
1897 1897 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1898 1898 end
1899 1899 end
1900 1900
1901 1901 def test_last_journal_id_with_journals_should_return_the_journal_id
1902 1902 assert_equal 2, Issue.find(1).last_journal_id
1903 1903 end
1904 1904
1905 1905 def test_last_journal_id_without_journals_should_return_nil
1906 1906 assert_nil Issue.find(3).last_journal_id
1907 1907 end
1908 1908
1909 1909 def test_journals_after_should_return_journals_with_greater_id
1910 1910 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1911 1911 assert_equal [], Issue.find(1).journals_after('2')
1912 1912 end
1913 1913
1914 1914 def test_journals_after_with_blank_arg_should_return_all_journals
1915 1915 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1916 1916 end
1917 1917
1918 1918 def test_css_classes_should_include_priority
1919 1919 issue = Issue.new(:priority => IssuePriority.find(8))
1920 1920 classes = issue.css_classes.split(' ')
1921 1921 assert_include 'priority-8', classes
1922 1922 assert_include 'priority-highest', classes
1923 1923 end
1924 1924
1925 1925 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
1926 1926 set_tmp_attachments_directory
1927 1927 issue = Issue.generate!
1928 1928 issue.save_attachments({
1929 1929 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
1930 1930 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
1931 1931 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
1932 1932 })
1933 1933 issue.attach_saved_attachments
1934 1934
1935 1935 assert_equal 3, issue.reload.attachments.count
1936 1936 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
1937 1937 end
1938 1938 end
General Comments 0
You need to be logged in to leave comments. Login now