##// END OF EJS Templates
Limits the tracker list in filters and issue counts (#285)....
Jean-Philippe Lang -
r15158:6cd84af522bb
parent child
Show More
@@ -1,234 +1,234
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :settings, :only => :settings
20 menu_item :settings, :only => :settings
21
21
22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize_global, :only => [:new, :create]
24 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 accept_rss_auth :index
26 accept_rss_auth :index
27 accept_api_auth :index, :show, :create, :update, :destroy
27 accept_api_auth :index, :show, :create, :update, :destroy
28 require_sudo_mode :destroy
28 require_sudo_mode :destroy
29
29
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
31 if controller.request.post?
31 if controller.request.post?
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
33 end
33 end
34 end
34 end
35
35
36 helper :custom_fields
36 helper :custom_fields
37 helper :issues
37 helper :issues
38 helper :queries
38 helper :queries
39 helper :repositories
39 helper :repositories
40 helper :members
40 helper :members
41
41
42 # Lists visible projects
42 # Lists visible projects
43 def index
43 def index
44 scope = Project.visible.sorted
44 scope = Project.visible.sorted
45
45
46 respond_to do |format|
46 respond_to do |format|
47 format.html {
47 format.html {
48 unless params[:closed]
48 unless params[:closed]
49 scope = scope.active
49 scope = scope.active
50 end
50 end
51 @projects = scope.to_a
51 @projects = scope.to_a
52 }
52 }
53 format.api {
53 format.api {
54 @offset, @limit = api_offset_and_limit
54 @offset, @limit = api_offset_and_limit
55 @project_count = scope.count
55 @project_count = scope.count
56 @projects = scope.offset(@offset).limit(@limit).to_a
56 @projects = scope.offset(@offset).limit(@limit).to_a
57 }
57 }
58 format.atom {
58 format.atom {
59 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
59 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 }
61 }
62 end
62 end
63 end
63 end
64
64
65 def new
65 def new
66 @issue_custom_fields = IssueCustomField.sorted.to_a
66 @issue_custom_fields = IssueCustomField.sorted.to_a
67 @trackers = Tracker.sorted.to_a
67 @trackers = Tracker.sorted.to_a
68 @project = Project.new
68 @project = Project.new
69 @project.safe_attributes = params[:project]
69 @project.safe_attributes = params[:project]
70 end
70 end
71
71
72 def create
72 def create
73 @issue_custom_fields = IssueCustomField.sorted.to_a
73 @issue_custom_fields = IssueCustomField.sorted.to_a
74 @trackers = Tracker.sorted.to_a
74 @trackers = Tracker.sorted.to_a
75 @project = Project.new
75 @project = Project.new
76 @project.safe_attributes = params[:project]
76 @project.safe_attributes = params[:project]
77
77
78 if @project.save
78 if @project.save
79 unless User.current.admin?
79 unless User.current.admin?
80 @project.add_default_member(User.current)
80 @project.add_default_member(User.current)
81 end
81 end
82 respond_to do |format|
82 respond_to do |format|
83 format.html {
83 format.html {
84 flash[:notice] = l(:notice_successful_create)
84 flash[:notice] = l(:notice_successful_create)
85 if params[:continue]
85 if params[:continue]
86 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
86 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
87 redirect_to new_project_path(attrs)
87 redirect_to new_project_path(attrs)
88 else
88 else
89 redirect_to settings_project_path(@project)
89 redirect_to settings_project_path(@project)
90 end
90 end
91 }
91 }
92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
93 end
93 end
94 else
94 else
95 respond_to do |format|
95 respond_to do |format|
96 format.html { render :action => 'new' }
96 format.html { render :action => 'new' }
97 format.api { render_validation_errors(@project) }
97 format.api { render_validation_errors(@project) }
98 end
98 end
99 end
99 end
100 end
100 end
101
101
102 def copy
102 def copy
103 @issue_custom_fields = IssueCustomField.sorted.to_a
103 @issue_custom_fields = IssueCustomField.sorted.to_a
104 @trackers = Tracker.sorted.to_a
104 @trackers = Tracker.sorted.to_a
105 @source_project = Project.find(params[:id])
105 @source_project = Project.find(params[:id])
106 if request.get?
106 if request.get?
107 @project = Project.copy_from(@source_project)
107 @project = Project.copy_from(@source_project)
108 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
108 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
109 else
109 else
110 Mailer.with_deliveries(params[:notifications] == '1') do
110 Mailer.with_deliveries(params[:notifications] == '1') do
111 @project = Project.new
111 @project = Project.new
112 @project.safe_attributes = params[:project]
112 @project.safe_attributes = params[:project]
113 if @project.copy(@source_project, :only => params[:only])
113 if @project.copy(@source_project, :only => params[:only])
114 flash[:notice] = l(:notice_successful_create)
114 flash[:notice] = l(:notice_successful_create)
115 redirect_to settings_project_path(@project)
115 redirect_to settings_project_path(@project)
116 elsif !@project.new_record?
116 elsif !@project.new_record?
117 # Project was created
117 # Project was created
118 # But some objects were not copied due to validation failures
118 # But some objects were not copied due to validation failures
119 # (eg. issues from disabled trackers)
119 # (eg. issues from disabled trackers)
120 # TODO: inform about that
120 # TODO: inform about that
121 redirect_to settings_project_path(@project)
121 redirect_to settings_project_path(@project)
122 end
122 end
123 end
123 end
124 end
124 end
125 rescue ActiveRecord::RecordNotFound
125 rescue ActiveRecord::RecordNotFound
126 # source_project not found
126 # source_project not found
127 render_404
127 render_404
128 end
128 end
129
129
130 # Show @project
130 # Show @project
131 def show
131 def show
132 # try to redirect to the requested menu item
132 # try to redirect to the requested menu item
133 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
133 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
134 return
134 return
135 end
135 end
136
136
137 @users_by_role = @project.users_by_role
137 @users_by_role = @project.users_by_role
138 @subprojects = @project.children.visible.to_a
138 @subprojects = @project.children.visible.to_a
139 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
139 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
140 @trackers = @project.rolled_up_trackers
140 @trackers = @project.rolled_up_trackers.visible
141
141
142 cond = @project.project_condition(Setting.display_subprojects_issues?)
142 cond = @project.project_condition(Setting.display_subprojects_issues?)
143
143
144 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
144 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
145 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
145 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
146
146
147 if User.current.allowed_to_view_all_time_entries?(@project)
147 if User.current.allowed_to_view_all_time_entries?(@project)
148 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
148 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
149 end
149 end
150
150
151 @key = User.current.rss_key
151 @key = User.current.rss_key
152
152
153 respond_to do |format|
153 respond_to do |format|
154 format.html
154 format.html
155 format.api
155 format.api
156 end
156 end
157 end
157 end
158
158
159 def settings
159 def settings
160 @issue_custom_fields = IssueCustomField.sorted.to_a
160 @issue_custom_fields = IssueCustomField.sorted.to_a
161 @issue_category ||= IssueCategory.new
161 @issue_category ||= IssueCategory.new
162 @member ||= @project.members.new
162 @member ||= @project.members.new
163 @trackers = Tracker.sorted.to_a
163 @trackers = Tracker.sorted.to_a
164 @wiki ||= @project.wiki || Wiki.new(:project => @project)
164 @wiki ||= @project.wiki || Wiki.new(:project => @project)
165 end
165 end
166
166
167 def edit
167 def edit
168 end
168 end
169
169
170 def update
170 def update
171 @project.safe_attributes = params[:project]
171 @project.safe_attributes = params[:project]
172 if @project.save
172 if @project.save
173 respond_to do |format|
173 respond_to do |format|
174 format.html {
174 format.html {
175 flash[:notice] = l(:notice_successful_update)
175 flash[:notice] = l(:notice_successful_update)
176 redirect_to settings_project_path(@project)
176 redirect_to settings_project_path(@project)
177 }
177 }
178 format.api { render_api_ok }
178 format.api { render_api_ok }
179 end
179 end
180 else
180 else
181 respond_to do |format|
181 respond_to do |format|
182 format.html {
182 format.html {
183 settings
183 settings
184 render :action => 'settings'
184 render :action => 'settings'
185 }
185 }
186 format.api { render_validation_errors(@project) }
186 format.api { render_validation_errors(@project) }
187 end
187 end
188 end
188 end
189 end
189 end
190
190
191 def modules
191 def modules
192 @project.enabled_module_names = params[:enabled_module_names]
192 @project.enabled_module_names = params[:enabled_module_names]
193 flash[:notice] = l(:notice_successful_update)
193 flash[:notice] = l(:notice_successful_update)
194 redirect_to settings_project_path(@project, :tab => 'modules')
194 redirect_to settings_project_path(@project, :tab => 'modules')
195 end
195 end
196
196
197 def archive
197 def archive
198 unless @project.archive
198 unless @project.archive
199 flash[:error] = l(:error_can_not_archive_project)
199 flash[:error] = l(:error_can_not_archive_project)
200 end
200 end
201 redirect_to admin_projects_path(:status => params[:status])
201 redirect_to admin_projects_path(:status => params[:status])
202 end
202 end
203
203
204 def unarchive
204 def unarchive
205 unless @project.active?
205 unless @project.active?
206 @project.unarchive
206 @project.unarchive
207 end
207 end
208 redirect_to admin_projects_path(:status => params[:status])
208 redirect_to admin_projects_path(:status => params[:status])
209 end
209 end
210
210
211 def close
211 def close
212 @project.close
212 @project.close
213 redirect_to project_path(@project)
213 redirect_to project_path(@project)
214 end
214 end
215
215
216 def reopen
216 def reopen
217 @project.reopen
217 @project.reopen
218 redirect_to project_path(@project)
218 redirect_to project_path(@project)
219 end
219 end
220
220
221 # Delete @project
221 # Delete @project
222 def destroy
222 def destroy
223 @project_to_destroy = @project
223 @project_to_destroy = @project
224 if api_request? || params[:confirm]
224 if api_request? || params[:confirm]
225 @project_to_destroy.destroy
225 @project_to_destroy.destroy
226 respond_to do |format|
226 respond_to do |format|
227 format.html { redirect_to admin_projects_path }
227 format.html { redirect_to admin_projects_path }
228 format.api { render_api_ok }
228 format.api { render_api_ok }
229 end
229 end
230 end
230 end
231 # hide project in layout
231 # hide project in layout
232 @project = nil
232 @project = nil
233 end
233 end
234 end
234 end
@@ -1,95 +1,95
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ReportsController < ApplicationController
18 class ReportsController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize, :find_issue_statuses
20 before_filter :find_project, :authorize, :find_issue_statuses
21
21
22 def issue_report
22 def issue_report
23 @trackers = @project.trackers
23 @trackers = @project.rolled_up_trackers(false).visible
24 @versions = @project.shared_versions.sort
24 @versions = @project.shared_versions.sort
25 @priorities = IssuePriority.all.reverse
25 @priorities = IssuePriority.all.reverse
26 @categories = @project.issue_categories
26 @categories = @project.issue_categories
27 @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
27 @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
28 @authors = @project.users.sort
28 @authors = @project.users.sort
29 @subprojects = @project.descendants.visible
29 @subprojects = @project.descendants.visible
30
30
31 @issues_by_tracker = Issue.by_tracker(@project)
31 @issues_by_tracker = Issue.by_tracker(@project)
32 @issues_by_version = Issue.by_version(@project)
32 @issues_by_version = Issue.by_version(@project)
33 @issues_by_priority = Issue.by_priority(@project)
33 @issues_by_priority = Issue.by_priority(@project)
34 @issues_by_category = Issue.by_category(@project)
34 @issues_by_category = Issue.by_category(@project)
35 @issues_by_assigned_to = Issue.by_assigned_to(@project)
35 @issues_by_assigned_to = Issue.by_assigned_to(@project)
36 @issues_by_author = Issue.by_author(@project)
36 @issues_by_author = Issue.by_author(@project)
37 @issues_by_subproject = Issue.by_subproject(@project) || []
37 @issues_by_subproject = Issue.by_subproject(@project) || []
38
38
39 render :template => "reports/issue_report"
39 render :template => "reports/issue_report"
40 end
40 end
41
41
42 def issue_report_details
42 def issue_report_details
43 case params[:detail]
43 case params[:detail]
44 when "tracker"
44 when "tracker"
45 @field = "tracker_id"
45 @field = "tracker_id"
46 @rows = @project.trackers
46 @rows = @project.rolled_up_trackers(false).visible
47 @data = Issue.by_tracker(@project)
47 @data = Issue.by_tracker(@project)
48 @report_title = l(:field_tracker)
48 @report_title = l(:field_tracker)
49 when "version"
49 when "version"
50 @field = "fixed_version_id"
50 @field = "fixed_version_id"
51 @rows = @project.shared_versions.sort
51 @rows = @project.shared_versions.sort
52 @data = Issue.by_version(@project)
52 @data = Issue.by_version(@project)
53 @report_title = l(:field_version)
53 @report_title = l(:field_version)
54 when "priority"
54 when "priority"
55 @field = "priority_id"
55 @field = "priority_id"
56 @rows = IssuePriority.all.reverse
56 @rows = IssuePriority.all.reverse
57 @data = Issue.by_priority(@project)
57 @data = Issue.by_priority(@project)
58 @report_title = l(:field_priority)
58 @report_title = l(:field_priority)
59 when "category"
59 when "category"
60 @field = "category_id"
60 @field = "category_id"
61 @rows = @project.issue_categories
61 @rows = @project.issue_categories
62 @data = Issue.by_category(@project)
62 @data = Issue.by_category(@project)
63 @report_title = l(:field_category)
63 @report_title = l(:field_category)
64 when "assigned_to"
64 when "assigned_to"
65 @field = "assigned_to_id"
65 @field = "assigned_to_id"
66 @rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
66 @rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
67 @data = Issue.by_assigned_to(@project)
67 @data = Issue.by_assigned_to(@project)
68 @report_title = l(:field_assigned_to)
68 @report_title = l(:field_assigned_to)
69 when "author"
69 when "author"
70 @field = "author_id"
70 @field = "author_id"
71 @rows = @project.users.sort
71 @rows = @project.users.sort
72 @data = Issue.by_author(@project)
72 @data = Issue.by_author(@project)
73 @report_title = l(:field_author)
73 @report_title = l(:field_author)
74 when "subproject"
74 when "subproject"
75 @field = "project_id"
75 @field = "project_id"
76 @rows = @project.descendants.visible
76 @rows = @project.descendants.visible
77 @data = Issue.by_subproject(@project) || []
77 @data = Issue.by_subproject(@project) || []
78 @report_title = l(:field_subproject)
78 @report_title = l(:field_subproject)
79 end
79 end
80
80
81 respond_to do |format|
81 respond_to do |format|
82 if @field
82 if @field
83 format.html {}
83 format.html {}
84 else
84 else
85 format.html { redirect_to :action => 'issue_report', :id => @project }
85 format.html { redirect_to :action => 'issue_report', :id => @project }
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 private
90 private
91
91
92 def find_issue_statuses
92 def find_issue_statuses
93 @statuses = IssueStatus.sorted.to_a
93 @statuses = IssueStatus.sorted.to_a
94 end
94 end
95 end
95 end
@@ -1,1047 +1,1055
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::NestedSet::ProjectNestedSet
20 include Redmine::NestedSet::ProjectNestedSet
21
21
22 # Project statuses
22 # Project statuses
23 STATUS_ACTIVE = 1
23 STATUS_ACTIVE = 1
24 STATUS_CLOSED = 5
24 STATUS_CLOSED = 5
25 STATUS_ARCHIVED = 9
25 STATUS_ARCHIVED = 9
26
26
27 # Maximum length for project identifiers
27 # Maximum length for project identifiers
28 IDENTIFIER_MAX_LENGTH = 100
28 IDENTIFIER_MAX_LENGTH = 100
29
29
30 # Specific overridden Activities
30 # Specific overridden Activities
31 has_many :time_entry_activities
31 has_many :time_entry_activities
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 # Memberships of active users only
33 # Memberships of active users only
34 has_many :members,
34 has_many :members,
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 has_many :issues, :dependent => :destroy
38 has_many :issues, :dependent => :destroy
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 belongs_to :default_version, :class_name => 'Version'
41 belongs_to :default_version, :class_name => 'Version'
42 has_many :time_entries, :dependent => :destroy
42 has_many :time_entries, :dependent => :destroy
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 has_one :repository, lambda {where(["is_default = ?", true])}
48 has_one :repository, lambda {where(["is_default = ?", true])}
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 lambda {order("#{CustomField.table_name}.position")},
54 lambda {order("#{CustomField.table_name}.position")},
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_attachable :view_permission => :view_files,
59 acts_as_attachable :view_permission => :view_files,
60 :edit_permission => :manage_files,
60 :edit_permission => :manage_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 validates_length_of :name, :maximum => 255
73 validates_length_of :name, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 # downcase letters, digits, dashes but not digits only
76 # downcase letters, digits, dashes but not digits only
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 # reserved words
78 # reserved words
79 validates_exclusion_of :identifier, :in => %w( new )
79 validates_exclusion_of :identifier, :in => %w( new )
80 validate :validate_parent
80 validate :validate_parent
81
81
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda {|mod|
87 scope :has_module, lambda {|mod|
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 }
89 }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :all_public, lambda { where(:is_public => true) }
92 scope :all_public, lambda { where(:is_public => true) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :allowed_to, lambda {|*args|
94 scope :allowed_to, lambda {|*args|
95 user = User.current
95 user = User.current
96 permission = nil
96 permission = nil
97 if args.first.is_a?(Symbol)
97 if args.first.is_a?(Symbol)
98 permission = args.shift
98 permission = args.shift
99 else
99 else
100 user = args.shift
100 user = args.shift
101 permission = args.shift
101 permission = args.shift
102 end
102 end
103 where(Project.allowed_to_condition(user, permission, *args))
103 where(Project.allowed_to_condition(user, permission, *args))
104 }
104 }
105 scope :like, lambda {|arg|
105 scope :like, lambda {|arg|
106 if arg.blank?
106 if arg.blank?
107 where(nil)
107 where(nil)
108 else
108 else
109 pattern = "%#{arg.to_s.strip.downcase}%"
109 pattern = "%#{arg.to_s.strip.downcase}%"
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 end
111 end
112 }
112 }
113 scope :sorted, lambda {order(:lft)}
113 scope :sorted, lambda {order(:lft)}
114 scope :having_trackers, lambda {
114 scope :having_trackers, lambda {
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 }
116 }
117
117
118 def initialize(attributes=nil, *args)
118 def initialize(attributes=nil, *args)
119 super
119 super
120
120
121 initialized = (attributes || {}).stringify_keys
121 initialized = (attributes || {}).stringify_keys
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 self.identifier = Project.next_identifier
123 self.identifier = Project.next_identifier
124 end
124 end
125 if !initialized.key?('is_public')
125 if !initialized.key?('is_public')
126 self.is_public = Setting.default_projects_public?
126 self.is_public = Setting.default_projects_public?
127 end
127 end
128 if !initialized.key?('enabled_module_names')
128 if !initialized.key?('enabled_module_names')
129 self.enabled_module_names = Setting.default_projects_modules
129 self.enabled_module_names = Setting.default_projects_modules
130 end
130 end
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 default = Setting.default_projects_tracker_ids
132 default = Setting.default_projects_tracker_ids
133 if default.is_a?(Array)
133 if default.is_a?(Array)
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 else
135 else
136 self.trackers = Tracker.sorted.to_a
136 self.trackers = Tracker.sorted.to_a
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 def identifier=(identifier)
141 def identifier=(identifier)
142 super unless identifier_frozen?
142 super unless identifier_frozen?
143 end
143 end
144
144
145 def identifier_frozen?
145 def identifier_frozen?
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 end
147 end
148
148
149 # returns latest created projects
149 # returns latest created projects
150 # non public projects will be returned only if user is a member of those
150 # non public projects will be returned only if user is a member of those
151 def self.latest(user=nil, count=5)
151 def self.latest(user=nil, count=5)
152 visible(user).limit(count).
152 visible(user).limit(count).
153 order(:created_on => :desc).
153 order(:created_on => :desc).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 to_a
155 to_a
156 end
156 end
157
157
158 # Returns true if the project is visible to +user+ or to the current user.
158 # Returns true if the project is visible to +user+ or to the current user.
159 def visible?(user=User.current)
159 def visible?(user=User.current)
160 user.allowed_to?(:view_project, self)
160 user.allowed_to?(:view_project, self)
161 end
161 end
162
162
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 #
164 #
165 # Examples:
165 # Examples:
166 # Project.visible_condition(admin) => "projects.status = 1"
166 # Project.visible_condition(admin) => "projects.status = 1"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 def self.visible_condition(user, options={})
169 def self.visible_condition(user, options={})
170 allowed_to_condition(user, :view_project, options)
170 allowed_to_condition(user, :view_project, options)
171 end
171 end
172
172
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 #
174 #
175 # Valid options:
175 # Valid options:
176 # * :project => limit the condition to project
176 # * :project => limit the condition to project
177 # * :with_subprojects => limit the condition to project and its subprojects
177 # * :with_subprojects => limit the condition to project and its subprojects
178 # * :member => limit the condition to the user projects
178 # * :member => limit the condition to the user projects
179 def self.allowed_to_condition(user, permission, options={})
179 def self.allowed_to_condition(user, permission, options={})
180 perm = Redmine::AccessControl.permission(permission)
180 perm = Redmine::AccessControl.permission(permission)
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 if perm && perm.project_module
182 if perm && perm.project_module
183 # If the permission belongs to a project module, make sure the module is enabled
183 # If the permission belongs to a project module, make sure the module is enabled
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 end
185 end
186 if project = options[:project]
186 if project = options[:project]
187 project_statement = project.project_condition(options[:with_subprojects])
187 project_statement = project.project_condition(options[:with_subprojects])
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 end
189 end
190
190
191 if user.admin?
191 if user.admin?
192 base_statement
192 base_statement
193 else
193 else
194 statement_by_role = {}
194 statement_by_role = {}
195 unless options[:member]
195 unless options[:member]
196 role = user.builtin_role
196 role = user.builtin_role
197 if role.allowed_to?(permission)
197 if role.allowed_to?(permission)
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 if user.id
199 if user.id
200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
201 end
201 end
202 statement_by_role[role] = s
202 statement_by_role[role] = s
203 end
203 end
204 end
204 end
205 user.projects_by_role.each do |role, projects|
205 user.projects_by_role.each do |role, projects|
206 if role.allowed_to?(permission) && projects.any?
206 if role.allowed_to?(permission) && projects.any?
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
208 end
208 end
209 end
209 end
210 if statement_by_role.empty?
210 if statement_by_role.empty?
211 "1=0"
211 "1=0"
212 else
212 else
213 if block_given?
213 if block_given?
214 statement_by_role.each do |role, statement|
214 statement_by_role.each do |role, statement|
215 if s = yield(role, user)
215 if s = yield(role, user)
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
217 end
217 end
218 end
218 end
219 end
219 end
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
221 end
221 end
222 end
222 end
223 end
223 end
224
224
225 def override_roles(role)
225 def override_roles(role)
226 @override_members ||= memberships.
226 @override_members ||= memberships.
227 joins(:principal).
227 joins(:principal).
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
229
229
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
232 member ? member.roles.to_a : [role]
232 member ? member.roles.to_a : [role]
233 end
233 end
234
234
235 def principals
235 def principals
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
237 end
237 end
238
238
239 def users
239 def users
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
241 end
241 end
242
242
243 # Returns the Systemwide and project specific activities
243 # Returns the Systemwide and project specific activities
244 def activities(include_inactive=false)
244 def activities(include_inactive=false)
245 t = TimeEntryActivity.table_name
245 t = TimeEntryActivity.table_name
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
247
247
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
249 if overridden_activity_ids.any?
249 if overridden_activity_ids.any?
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
251 end
251 end
252 unless include_inactive
252 unless include_inactive
253 scope = scope.active
253 scope = scope.active
254 end
254 end
255 scope
255 scope
256 end
256 end
257
257
258 # Will create a new Project specific Activity or update an existing one
258 # Will create a new Project specific Activity or update an existing one
259 #
259 #
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
261 # does not successfully save.
261 # does not successfully save.
262 def update_or_create_time_entry_activity(id, activity_hash)
262 def update_or_create_time_entry_activity(id, activity_hash)
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
264 self.create_time_entry_activity_if_needed(activity_hash)
264 self.create_time_entry_activity_if_needed(activity_hash)
265 else
265 else
266 activity = project.time_entry_activities.find_by_id(id.to_i)
266 activity = project.time_entry_activities.find_by_id(id.to_i)
267 activity.update_attributes(activity_hash) if activity
267 activity.update_attributes(activity_hash) if activity
268 end
268 end
269 end
269 end
270
270
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
272 #
272 #
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
274 # does not successfully save.
274 # does not successfully save.
275 def create_time_entry_activity_if_needed(activity)
275 def create_time_entry_activity_if_needed(activity)
276 if activity['parent_id']
276 if activity['parent_id']
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
278 activity['name'] = parent_activity.name
278 activity['name'] = parent_activity.name
279 activity['position'] = parent_activity.position
279 activity['position'] = parent_activity.position
280 if Enumeration.overriding_change?(activity, parent_activity)
280 if Enumeration.overriding_change?(activity, parent_activity)
281 project_activity = self.time_entry_activities.create(activity)
281 project_activity = self.time_entry_activities.create(activity)
282 if project_activity.new_record?
282 if project_activity.new_record?
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
284 else
284 else
285 self.time_entries.
285 self.time_entries.
286 where(:activity_id => parent_activity.id).
286 where(:activity_id => parent_activity.id).
287 update_all(:activity_id => project_activity.id)
287 update_all(:activity_id => project_activity.id)
288 end
288 end
289 end
289 end
290 end
290 end
291 end
291 end
292
292
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
294 #
294 #
295 # Examples:
295 # Examples:
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
297 # project.project_condition(false) => "projects.id = 1"
297 # project.project_condition(false) => "projects.id = 1"
298 def project_condition(with_subprojects)
298 def project_condition(with_subprojects)
299 cond = "#{Project.table_name}.id = #{id}"
299 cond = "#{Project.table_name}.id = #{id}"
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
301 cond
301 cond
302 end
302 end
303
303
304 def self.find(*args)
304 def self.find(*args)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
306 project = find_by_identifier(*args)
306 project = find_by_identifier(*args)
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
308 project
308 project
309 else
309 else
310 super
310 super
311 end
311 end
312 end
312 end
313
313
314 def self.find_by_param(*args)
314 def self.find_by_param(*args)
315 self.find(*args)
315 self.find(*args)
316 end
316 end
317
317
318 alias :base_reload :reload
318 alias :base_reload :reload
319 def reload(*args)
319 def reload(*args)
320 @principals = nil
320 @principals = nil
321 @users = nil
321 @users = nil
322 @shared_versions = nil
322 @shared_versions = nil
323 @rolled_up_versions = nil
323 @rolled_up_versions = nil
324 @rolled_up_trackers = nil
324 @rolled_up_trackers = nil
325 @all_issue_custom_fields = nil
325 @all_issue_custom_fields = nil
326 @all_time_entry_custom_fields = nil
326 @all_time_entry_custom_fields = nil
327 @to_param = nil
327 @to_param = nil
328 @allowed_parents = nil
328 @allowed_parents = nil
329 @allowed_permissions = nil
329 @allowed_permissions = nil
330 @actions_allowed = nil
330 @actions_allowed = nil
331 @start_date = nil
331 @start_date = nil
332 @due_date = nil
332 @due_date = nil
333 @override_members = nil
333 @override_members = nil
334 @assignable_users = nil
334 @assignable_users = nil
335 base_reload(*args)
335 base_reload(*args)
336 end
336 end
337
337
338 def to_param
338 def to_param
339 if new_record?
339 if new_record?
340 nil
340 nil
341 else
341 else
342 # id is used for projects with a numeric identifier (compatibility)
342 # id is used for projects with a numeric identifier (compatibility)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
344 end
344 end
345 end
345 end
346
346
347 def active?
347 def active?
348 self.status == STATUS_ACTIVE
348 self.status == STATUS_ACTIVE
349 end
349 end
350
350
351 def archived?
351 def archived?
352 self.status == STATUS_ARCHIVED
352 self.status == STATUS_ARCHIVED
353 end
353 end
354
354
355 # Archives the project and its descendants
355 # Archives the project and its descendants
356 def archive
356 def archive
357 # Check that there is no issue of a non descendant project that is assigned
357 # Check that there is no issue of a non descendant project that is assigned
358 # to one of the project or descendant versions
358 # to one of the project or descendant versions
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
360
360
361 if version_ids.any? &&
361 if version_ids.any? &&
362 Issue.
362 Issue.
363 includes(:project).
363 includes(:project).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
365 where(:fixed_version_id => version_ids).
365 where(:fixed_version_id => version_ids).
366 exists?
366 exists?
367 return false
367 return false
368 end
368 end
369 Project.transaction do
369 Project.transaction do
370 archive!
370 archive!
371 end
371 end
372 true
372 true
373 end
373 end
374
374
375 # Unarchives the project
375 # Unarchives the project
376 # All its ancestors must be active
376 # All its ancestors must be active
377 def unarchive
377 def unarchive
378 return false if ancestors.detect {|a| !a.active?}
378 return false if ancestors.detect {|a| !a.active?}
379 update_attribute :status, STATUS_ACTIVE
379 update_attribute :status, STATUS_ACTIVE
380 end
380 end
381
381
382 def close
382 def close
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
384 end
384 end
385
385
386 def reopen
386 def reopen
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
388 end
388 end
389
389
390 # Returns an array of projects the project can be moved to
390 # Returns an array of projects the project can be moved to
391 # by the current user
391 # by the current user
392 def allowed_parents(user=User.current)
392 def allowed_parents(user=User.current)
393 return @allowed_parents if @allowed_parents
393 return @allowed_parents if @allowed_parents
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
395 @allowed_parents = @allowed_parents - self_and_descendants
395 @allowed_parents = @allowed_parents - self_and_descendants
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
397 @allowed_parents << nil
397 @allowed_parents << nil
398 end
398 end
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
400 @allowed_parents << parent
400 @allowed_parents << parent
401 end
401 end
402 @allowed_parents
402 @allowed_parents
403 end
403 end
404
404
405 # Sets the parent of the project with authorization check
405 # Sets the parent of the project with authorization check
406 def set_allowed_parent!(p)
406 def set_allowed_parent!(p)
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
408 p = p.id if p.is_a?(Project)
408 p = p.id if p.is_a?(Project)
409 send :safe_attributes, {:project_id => p}
409 send :safe_attributes, {:project_id => p}
410 save
410 save
411 end
411 end
412
412
413 # Sets the parent of the project and saves the project
413 # Sets the parent of the project and saves the project
414 # Argument can be either a Project, a String, a Fixnum or nil
414 # Argument can be either a Project, a String, a Fixnum or nil
415 def set_parent!(p)
415 def set_parent!(p)
416 if p.is_a?(Project)
416 if p.is_a?(Project)
417 self.parent = p
417 self.parent = p
418 else
418 else
419 self.parent_id = p
419 self.parent_id = p
420 end
420 end
421 save
421 save
422 end
422 end
423
423
424 # Returns an array of the trackers used by the project and its active sub projects
424 # Returns a scope of the trackers used by the project and its active sub projects
425 def rolled_up_trackers
425 def rolled_up_trackers(include_subprojects=true)
426 @rolled_up_trackers ||=
426 if include_subprojects
427 Tracker.
427 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
428 joins(projects: :enabled_modules).
428 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
429 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
429 else
430 where("#{EnabledModule.table_name}.name = ?", 'issue_tracking').
430 rolled_up_trackers_base_scope.
431 uniq.
431 where(:projects => {:id => id})
432 sorted.
432 end
433 to_a
433 end
434
435 def rolled_up_trackers_base_scope
436 Tracker.
437 joins(projects: :enabled_modules).
438 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
439 where(:enabled_modules => {:name => 'issue_tracking'}).
440 uniq.
441 sorted
434 end
442 end
435
443
436 # Closes open and locked project versions that are completed
444 # Closes open and locked project versions that are completed
437 def close_completed_versions
445 def close_completed_versions
438 Version.transaction do
446 Version.transaction do
439 versions.where(:status => %w(open locked)).each do |version|
447 versions.where(:status => %w(open locked)).each do |version|
440 if version.completed?
448 if version.completed?
441 version.update_attribute(:status, 'closed')
449 version.update_attribute(:status, 'closed')
442 end
450 end
443 end
451 end
444 end
452 end
445 end
453 end
446
454
447 # Returns a scope of the Versions on subprojects
455 # Returns a scope of the Versions on subprojects
448 def rolled_up_versions
456 def rolled_up_versions
449 @rolled_up_versions ||=
457 @rolled_up_versions ||=
450 Version.
458 Version.
451 joins(:project).
459 joins(:project).
452 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
460 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
453 end
461 end
454
462
455 # Returns a scope of the Versions used by the project
463 # Returns a scope of the Versions used by the project
456 def shared_versions
464 def shared_versions
457 if new_record?
465 if new_record?
458 Version.
466 Version.
459 joins(:project).
467 joins(:project).
460 preload(:project).
468 preload(:project).
461 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
462 else
470 else
463 @shared_versions ||= begin
471 @shared_versions ||= begin
464 r = root? ? self : root
472 r = root? ? self : root
465 Version.
473 Version.
466 joins(:project).
474 joins(:project).
467 preload(:project).
475 preload(:project).
468 where("#{Project.table_name}.id = #{id}" +
476 where("#{Project.table_name}.id = #{id}" +
469 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
477 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
470 " #{Version.table_name}.sharing = 'system'" +
478 " #{Version.table_name}.sharing = 'system'" +
471 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
479 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
472 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
480 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
473 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
481 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
474 "))")
482 "))")
475 end
483 end
476 end
484 end
477 end
485 end
478
486
479 # Returns a hash of project users grouped by role
487 # Returns a hash of project users grouped by role
480 def users_by_role
488 def users_by_role
481 members.includes(:user, :roles).inject({}) do |h, m|
489 members.includes(:user, :roles).inject({}) do |h, m|
482 m.roles.each do |r|
490 m.roles.each do |r|
483 h[r] ||= []
491 h[r] ||= []
484 h[r] << m.user
492 h[r] << m.user
485 end
493 end
486 h
494 h
487 end
495 end
488 end
496 end
489
497
490 # Adds user as a project member with the default role
498 # Adds user as a project member with the default role
491 # Used for when a non-admin user creates a project
499 # Used for when a non-admin user creates a project
492 def add_default_member(user)
500 def add_default_member(user)
493 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
501 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
494 member = Member.new(:project => self, :principal => user, :roles => [role])
502 member = Member.new(:project => self, :principal => user, :roles => [role])
495 self.members << member
503 self.members << member
496 member
504 member
497 end
505 end
498
506
499 # Deletes all project's members
507 # Deletes all project's members
500 def delete_all_members
508 def delete_all_members
501 me, mr = Member.table_name, MemberRole.table_name
509 me, mr = Member.table_name, MemberRole.table_name
502 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
510 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
503 Member.delete_all(['project_id = ?', id])
511 Member.delete_all(['project_id = ?', id])
504 end
512 end
505
513
506 # Return a Principal scope of users/groups issues can be assigned to
514 # Return a Principal scope of users/groups issues can be assigned to
507 def assignable_users
515 def assignable_users
508 types = ['User']
516 types = ['User']
509 types << 'Group' if Setting.issue_group_assignment?
517 types << 'Group' if Setting.issue_group_assignment?
510
518
511 @assignable_users ||= Principal.
519 @assignable_users ||= Principal.
512 active.
520 active.
513 joins(:members => :roles).
521 joins(:members => :roles).
514 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
522 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
515 uniq.
523 uniq.
516 sorted
524 sorted
517 end
525 end
518
526
519 # Returns the mail addresses of users that should be always notified on project events
527 # Returns the mail addresses of users that should be always notified on project events
520 def recipients
528 def recipients
521 notified_users.collect {|user| user.mail}
529 notified_users.collect {|user| user.mail}
522 end
530 end
523
531
524 # Returns the users that should be notified on project events
532 # Returns the users that should be notified on project events
525 def notified_users
533 def notified_users
526 # TODO: User part should be extracted to User#notify_about?
534 # TODO: User part should be extracted to User#notify_about?
527 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
535 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
528 end
536 end
529
537
530 # Returns a scope of all custom fields enabled for project issues
538 # Returns a scope of all custom fields enabled for project issues
531 # (explicitly associated custom fields and custom fields enabled for all projects)
539 # (explicitly associated custom fields and custom fields enabled for all projects)
532 def all_issue_custom_fields
540 def all_issue_custom_fields
533 if new_record?
541 if new_record?
534 @all_issue_custom_fields ||= IssueCustomField.
542 @all_issue_custom_fields ||= IssueCustomField.
535 sorted.
543 sorted.
536 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
544 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
537 else
545 else
538 @all_issue_custom_fields ||= IssueCustomField.
546 @all_issue_custom_fields ||= IssueCustomField.
539 sorted.
547 sorted.
540 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
548 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
541 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
549 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
542 " WHERE cfp.project_id = ?)", true, id)
550 " WHERE cfp.project_id = ?)", true, id)
543 end
551 end
544 end
552 end
545
553
546 def project
554 def project
547 self
555 self
548 end
556 end
549
557
550 def <=>(project)
558 def <=>(project)
551 name.casecmp(project.name)
559 name.casecmp(project.name)
552 end
560 end
553
561
554 def to_s
562 def to_s
555 name
563 name
556 end
564 end
557
565
558 # Returns a short description of the projects (first lines)
566 # Returns a short description of the projects (first lines)
559 def short_description(length = 255)
567 def short_description(length = 255)
560 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
568 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
561 end
569 end
562
570
563 def css_classes
571 def css_classes
564 s = 'project'
572 s = 'project'
565 s << ' root' if root?
573 s << ' root' if root?
566 s << ' child' if child?
574 s << ' child' if child?
567 s << (leaf? ? ' leaf' : ' parent')
575 s << (leaf? ? ' leaf' : ' parent')
568 unless active?
576 unless active?
569 if archived?
577 if archived?
570 s << ' archived'
578 s << ' archived'
571 else
579 else
572 s << ' closed'
580 s << ' closed'
573 end
581 end
574 end
582 end
575 s
583 s
576 end
584 end
577
585
578 # The earliest start date of a project, based on it's issues and versions
586 # The earliest start date of a project, based on it's issues and versions
579 def start_date
587 def start_date
580 @start_date ||= [
588 @start_date ||= [
581 issues.minimum('start_date'),
589 issues.minimum('start_date'),
582 shared_versions.minimum('effective_date'),
590 shared_versions.minimum('effective_date'),
583 Issue.fixed_version(shared_versions).minimum('start_date')
591 Issue.fixed_version(shared_versions).minimum('start_date')
584 ].compact.min
592 ].compact.min
585 end
593 end
586
594
587 # The latest due date of an issue or version
595 # The latest due date of an issue or version
588 def due_date
596 def due_date
589 @due_date ||= [
597 @due_date ||= [
590 issues.maximum('due_date'),
598 issues.maximum('due_date'),
591 shared_versions.maximum('effective_date'),
599 shared_versions.maximum('effective_date'),
592 Issue.fixed_version(shared_versions).maximum('due_date')
600 Issue.fixed_version(shared_versions).maximum('due_date')
593 ].compact.max
601 ].compact.max
594 end
602 end
595
603
596 def overdue?
604 def overdue?
597 active? && !due_date.nil? && (due_date < User.current.today)
605 active? && !due_date.nil? && (due_date < User.current.today)
598 end
606 end
599
607
600 # Returns the percent completed for this project, based on the
608 # Returns the percent completed for this project, based on the
601 # progress on it's versions.
609 # progress on it's versions.
602 def completed_percent(options={:include_subprojects => false})
610 def completed_percent(options={:include_subprojects => false})
603 if options.delete(:include_subprojects)
611 if options.delete(:include_subprojects)
604 total = self_and_descendants.collect(&:completed_percent).sum
612 total = self_and_descendants.collect(&:completed_percent).sum
605
613
606 total / self_and_descendants.count
614 total / self_and_descendants.count
607 else
615 else
608 if versions.count > 0
616 if versions.count > 0
609 total = versions.collect(&:completed_percent).sum
617 total = versions.collect(&:completed_percent).sum
610
618
611 total / versions.count
619 total / versions.count
612 else
620 else
613 100
621 100
614 end
622 end
615 end
623 end
616 end
624 end
617
625
618 # Return true if this project allows to do the specified action.
626 # Return true if this project allows to do the specified action.
619 # action can be:
627 # action can be:
620 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
628 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
621 # * a permission Symbol (eg. :edit_project)
629 # * a permission Symbol (eg. :edit_project)
622 def allows_to?(action)
630 def allows_to?(action)
623 if archived?
631 if archived?
624 # No action allowed on archived projects
632 # No action allowed on archived projects
625 return false
633 return false
626 end
634 end
627 unless active? || Redmine::AccessControl.read_action?(action)
635 unless active? || Redmine::AccessControl.read_action?(action)
628 # No write action allowed on closed projects
636 # No write action allowed on closed projects
629 return false
637 return false
630 end
638 end
631 # No action allowed on disabled modules
639 # No action allowed on disabled modules
632 if action.is_a? Hash
640 if action.is_a? Hash
633 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
641 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
634 else
642 else
635 allowed_permissions.include? action
643 allowed_permissions.include? action
636 end
644 end
637 end
645 end
638
646
639 # Return the enabled module with the given name
647 # Return the enabled module with the given name
640 # or nil if the module is not enabled for the project
648 # or nil if the module is not enabled for the project
641 def enabled_module(name)
649 def enabled_module(name)
642 name = name.to_s
650 name = name.to_s
643 enabled_modules.detect {|m| m.name == name}
651 enabled_modules.detect {|m| m.name == name}
644 end
652 end
645
653
646 # Return true if the module with the given name is enabled
654 # Return true if the module with the given name is enabled
647 def module_enabled?(name)
655 def module_enabled?(name)
648 enabled_module(name).present?
656 enabled_module(name).present?
649 end
657 end
650
658
651 def enabled_module_names=(module_names)
659 def enabled_module_names=(module_names)
652 if module_names && module_names.is_a?(Array)
660 if module_names && module_names.is_a?(Array)
653 module_names = module_names.collect(&:to_s).reject(&:blank?)
661 module_names = module_names.collect(&:to_s).reject(&:blank?)
654 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
662 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
655 else
663 else
656 enabled_modules.clear
664 enabled_modules.clear
657 end
665 end
658 end
666 end
659
667
660 # Returns an array of the enabled modules names
668 # Returns an array of the enabled modules names
661 def enabled_module_names
669 def enabled_module_names
662 enabled_modules.collect(&:name)
670 enabled_modules.collect(&:name)
663 end
671 end
664
672
665 # Enable a specific module
673 # Enable a specific module
666 #
674 #
667 # Examples:
675 # Examples:
668 # project.enable_module!(:issue_tracking)
676 # project.enable_module!(:issue_tracking)
669 # project.enable_module!("issue_tracking")
677 # project.enable_module!("issue_tracking")
670 def enable_module!(name)
678 def enable_module!(name)
671 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
679 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
672 end
680 end
673
681
674 # Disable a module if it exists
682 # Disable a module if it exists
675 #
683 #
676 # Examples:
684 # Examples:
677 # project.disable_module!(:issue_tracking)
685 # project.disable_module!(:issue_tracking)
678 # project.disable_module!("issue_tracking")
686 # project.disable_module!("issue_tracking")
679 # project.disable_module!(project.enabled_modules.first)
687 # project.disable_module!(project.enabled_modules.first)
680 def disable_module!(target)
688 def disable_module!(target)
681 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
689 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
682 target.destroy unless target.blank?
690 target.destroy unless target.blank?
683 end
691 end
684
692
685 safe_attributes 'name',
693 safe_attributes 'name',
686 'description',
694 'description',
687 'homepage',
695 'homepage',
688 'is_public',
696 'is_public',
689 'identifier',
697 'identifier',
690 'custom_field_values',
698 'custom_field_values',
691 'custom_fields',
699 'custom_fields',
692 'tracker_ids',
700 'tracker_ids',
693 'issue_custom_field_ids',
701 'issue_custom_field_ids',
694 'parent_id',
702 'parent_id',
695 'default_version_id'
703 'default_version_id'
696
704
697 safe_attributes 'enabled_module_names',
705 safe_attributes 'enabled_module_names',
698 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
706 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
699
707
700 safe_attributes 'inherit_members',
708 safe_attributes 'inherit_members',
701 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
709 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
702
710
703 def safe_attributes=(attrs, user=User.current)
711 def safe_attributes=(attrs, user=User.current)
704 return unless attrs.is_a?(Hash)
712 return unless attrs.is_a?(Hash)
705 attrs = attrs.deep_dup
713 attrs = attrs.deep_dup
706
714
707 @unallowed_parent_id = nil
715 @unallowed_parent_id = nil
708 if new_record? || attrs.key?('parent_id')
716 if new_record? || attrs.key?('parent_id')
709 parent_id_param = attrs['parent_id'].to_s
717 parent_id_param = attrs['parent_id'].to_s
710 if new_record? || parent_id_param != parent_id.to_s
718 if new_record? || parent_id_param != parent_id.to_s
711 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
719 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
712 unless allowed_parents(user).include?(p)
720 unless allowed_parents(user).include?(p)
713 attrs.delete('parent_id')
721 attrs.delete('parent_id')
714 @unallowed_parent_id = true
722 @unallowed_parent_id = true
715 end
723 end
716 end
724 end
717 end
725 end
718
726
719 super(attrs, user)
727 super(attrs, user)
720 end
728 end
721
729
722 # Returns an auto-generated project identifier based on the last identifier used
730 # Returns an auto-generated project identifier based on the last identifier used
723 def self.next_identifier
731 def self.next_identifier
724 p = Project.order('id DESC').first
732 p = Project.order('id DESC').first
725 p.nil? ? nil : p.identifier.to_s.succ
733 p.nil? ? nil : p.identifier.to_s.succ
726 end
734 end
727
735
728 # Copies and saves the Project instance based on the +project+.
736 # Copies and saves the Project instance based on the +project+.
729 # Duplicates the source project's:
737 # Duplicates the source project's:
730 # * Wiki
738 # * Wiki
731 # * Versions
739 # * Versions
732 # * Categories
740 # * Categories
733 # * Issues
741 # * Issues
734 # * Members
742 # * Members
735 # * Queries
743 # * Queries
736 #
744 #
737 # Accepts an +options+ argument to specify what to copy
745 # Accepts an +options+ argument to specify what to copy
738 #
746 #
739 # Examples:
747 # Examples:
740 # project.copy(1) # => copies everything
748 # project.copy(1) # => copies everything
741 # project.copy(1, :only => 'members') # => copies members only
749 # project.copy(1, :only => 'members') # => copies members only
742 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
750 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
743 def copy(project, options={})
751 def copy(project, options={})
744 project = project.is_a?(Project) ? project : Project.find(project)
752 project = project.is_a?(Project) ? project : Project.find(project)
745
753
746 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
754 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
747 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
755 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
748
756
749 Project.transaction do
757 Project.transaction do
750 if save
758 if save
751 reload
759 reload
752 to_be_copied.each do |name|
760 to_be_copied.each do |name|
753 send "copy_#{name}", project
761 send "copy_#{name}", project
754 end
762 end
755 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
763 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
756 save
764 save
757 else
765 else
758 false
766 false
759 end
767 end
760 end
768 end
761 end
769 end
762
770
763 def member_principals
771 def member_principals
764 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
772 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
765 memberships.active
773 memberships.active
766 end
774 end
767
775
768 # Returns a new unsaved Project instance with attributes copied from +project+
776 # Returns a new unsaved Project instance with attributes copied from +project+
769 def self.copy_from(project)
777 def self.copy_from(project)
770 project = project.is_a?(Project) ? project : Project.find(project)
778 project = project.is_a?(Project) ? project : Project.find(project)
771 # clear unique attributes
779 # clear unique attributes
772 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
780 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
773 copy = Project.new(attributes)
781 copy = Project.new(attributes)
774 copy.enabled_module_names = project.enabled_module_names
782 copy.enabled_module_names = project.enabled_module_names
775 copy.trackers = project.trackers
783 copy.trackers = project.trackers
776 copy.custom_values = project.custom_values.collect {|v| v.clone}
784 copy.custom_values = project.custom_values.collect {|v| v.clone}
777 copy.issue_custom_fields = project.issue_custom_fields
785 copy.issue_custom_fields = project.issue_custom_fields
778 copy
786 copy
779 end
787 end
780
788
781 # Yields the given block for each project with its level in the tree
789 # Yields the given block for each project with its level in the tree
782 def self.project_tree(projects, &block)
790 def self.project_tree(projects, &block)
783 ancestors = []
791 ancestors = []
784 projects.sort_by(&:lft).each do |project|
792 projects.sort_by(&:lft).each do |project|
785 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
793 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
786 ancestors.pop
794 ancestors.pop
787 end
795 end
788 yield project, ancestors.size
796 yield project, ancestors.size
789 ancestors << project
797 ancestors << project
790 end
798 end
791 end
799 end
792
800
793 private
801 private
794
802
795 def update_inherited_members
803 def update_inherited_members
796 if parent
804 if parent
797 if inherit_members? && !inherit_members_was
805 if inherit_members? && !inherit_members_was
798 remove_inherited_member_roles
806 remove_inherited_member_roles
799 add_inherited_member_roles
807 add_inherited_member_roles
800 elsif !inherit_members? && inherit_members_was
808 elsif !inherit_members? && inherit_members_was
801 remove_inherited_member_roles
809 remove_inherited_member_roles
802 end
810 end
803 end
811 end
804 end
812 end
805
813
806 def remove_inherited_member_roles
814 def remove_inherited_member_roles
807 member_roles = memberships.map(&:member_roles).flatten
815 member_roles = memberships.map(&:member_roles).flatten
808 member_role_ids = member_roles.map(&:id)
816 member_role_ids = member_roles.map(&:id)
809 member_roles.each do |member_role|
817 member_roles.each do |member_role|
810 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
818 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
811 member_role.destroy
819 member_role.destroy
812 end
820 end
813 end
821 end
814 end
822 end
815
823
816 def add_inherited_member_roles
824 def add_inherited_member_roles
817 if inherit_members? && parent
825 if inherit_members? && parent
818 parent.memberships.each do |parent_member|
826 parent.memberships.each do |parent_member|
819 member = Member.find_or_new(self.id, parent_member.user_id)
827 member = Member.find_or_new(self.id, parent_member.user_id)
820 parent_member.member_roles.each do |parent_member_role|
828 parent_member.member_roles.each do |parent_member_role|
821 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
829 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
822 end
830 end
823 member.save!
831 member.save!
824 end
832 end
825 memberships.reset
833 memberships.reset
826 end
834 end
827 end
835 end
828
836
829 def update_versions_from_hierarchy_change
837 def update_versions_from_hierarchy_change
830 Issue.update_versions_from_hierarchy_change(self)
838 Issue.update_versions_from_hierarchy_change(self)
831 end
839 end
832
840
833 def validate_parent
841 def validate_parent
834 if @unallowed_parent_id
842 if @unallowed_parent_id
835 errors.add(:parent_id, :invalid)
843 errors.add(:parent_id, :invalid)
836 elsif parent_id_changed?
844 elsif parent_id_changed?
837 unless parent.nil? || (parent.active? && move_possible?(parent))
845 unless parent.nil? || (parent.active? && move_possible?(parent))
838 errors.add(:parent_id, :invalid)
846 errors.add(:parent_id, :invalid)
839 end
847 end
840 end
848 end
841 end
849 end
842
850
843 # Copies wiki from +project+
851 # Copies wiki from +project+
844 def copy_wiki(project)
852 def copy_wiki(project)
845 # Check that the source project has a wiki first
853 # Check that the source project has a wiki first
846 unless project.wiki.nil?
854 unless project.wiki.nil?
847 wiki = self.wiki || Wiki.new
855 wiki = self.wiki || Wiki.new
848 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
856 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
849 wiki_pages_map = {}
857 wiki_pages_map = {}
850 project.wiki.pages.each do |page|
858 project.wiki.pages.each do |page|
851 # Skip pages without content
859 # Skip pages without content
852 next if page.content.nil?
860 next if page.content.nil?
853 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
861 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
854 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
862 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
855 new_wiki_page.content = new_wiki_content
863 new_wiki_page.content = new_wiki_content
856 wiki.pages << new_wiki_page
864 wiki.pages << new_wiki_page
857 wiki_pages_map[page.id] = new_wiki_page
865 wiki_pages_map[page.id] = new_wiki_page
858 end
866 end
859
867
860 self.wiki = wiki
868 self.wiki = wiki
861 wiki.save
869 wiki.save
862 # Reproduce page hierarchy
870 # Reproduce page hierarchy
863 project.wiki.pages.each do |page|
871 project.wiki.pages.each do |page|
864 if page.parent_id && wiki_pages_map[page.id]
872 if page.parent_id && wiki_pages_map[page.id]
865 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
873 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
866 wiki_pages_map[page.id].save
874 wiki_pages_map[page.id].save
867 end
875 end
868 end
876 end
869 end
877 end
870 end
878 end
871
879
872 # Copies versions from +project+
880 # Copies versions from +project+
873 def copy_versions(project)
881 def copy_versions(project)
874 project.versions.each do |version|
882 project.versions.each do |version|
875 new_version = Version.new
883 new_version = Version.new
876 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
884 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
877 self.versions << new_version
885 self.versions << new_version
878 end
886 end
879 end
887 end
880
888
881 # Copies issue categories from +project+
889 # Copies issue categories from +project+
882 def copy_issue_categories(project)
890 def copy_issue_categories(project)
883 project.issue_categories.each do |issue_category|
891 project.issue_categories.each do |issue_category|
884 new_issue_category = IssueCategory.new
892 new_issue_category = IssueCategory.new
885 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
893 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
886 self.issue_categories << new_issue_category
894 self.issue_categories << new_issue_category
887 end
895 end
888 end
896 end
889
897
890 # Copies issues from +project+
898 # Copies issues from +project+
891 def copy_issues(project)
899 def copy_issues(project)
892 # Stores the source issue id as a key and the copied issues as the
900 # Stores the source issue id as a key and the copied issues as the
893 # value. Used to map the two together for issue relations.
901 # value. Used to map the two together for issue relations.
894 issues_map = {}
902 issues_map = {}
895
903
896 # Store status and reopen locked/closed versions
904 # Store status and reopen locked/closed versions
897 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
905 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
898 version_statuses.each do |version, status|
906 version_statuses.each do |version, status|
899 version.update_attribute :status, 'open'
907 version.update_attribute :status, 'open'
900 end
908 end
901
909
902 # Get issues sorted by root_id, lft so that parent issues
910 # Get issues sorted by root_id, lft so that parent issues
903 # get copied before their children
911 # get copied before their children
904 project.issues.reorder('root_id, lft').each do |issue|
912 project.issues.reorder('root_id, lft').each do |issue|
905 new_issue = Issue.new
913 new_issue = Issue.new
906 new_issue.copy_from(issue, :subtasks => false, :link => false)
914 new_issue.copy_from(issue, :subtasks => false, :link => false)
907 new_issue.project = self
915 new_issue.project = self
908 # Changing project resets the custom field values
916 # Changing project resets the custom field values
909 # TODO: handle this in Issue#project=
917 # TODO: handle this in Issue#project=
910 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
918 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
911 # Reassign fixed_versions by name, since names are unique per project
919 # Reassign fixed_versions by name, since names are unique per project
912 if issue.fixed_version && issue.fixed_version.project == project
920 if issue.fixed_version && issue.fixed_version.project == project
913 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
921 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
914 end
922 end
915 # Reassign version custom field values
923 # Reassign version custom field values
916 new_issue.custom_field_values.each do |custom_value|
924 new_issue.custom_field_values.each do |custom_value|
917 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
925 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
918 versions = Version.where(:id => custom_value.value).to_a
926 versions = Version.where(:id => custom_value.value).to_a
919 new_value = versions.map do |version|
927 new_value = versions.map do |version|
920 if version.project == project
928 if version.project == project
921 self.versions.detect {|v| v.name == version.name}.try(:id)
929 self.versions.detect {|v| v.name == version.name}.try(:id)
922 else
930 else
923 version.id
931 version.id
924 end
932 end
925 end
933 end
926 new_value.compact!
934 new_value.compact!
927 new_value = new_value.first unless custom_value.custom_field.multiple?
935 new_value = new_value.first unless custom_value.custom_field.multiple?
928 custom_value.value = new_value
936 custom_value.value = new_value
929 end
937 end
930 end
938 end
931 # Reassign the category by name, since names are unique per project
939 # Reassign the category by name, since names are unique per project
932 if issue.category
940 if issue.category
933 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
941 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
934 end
942 end
935 # Parent issue
943 # Parent issue
936 if issue.parent_id
944 if issue.parent_id
937 if copied_parent = issues_map[issue.parent_id]
945 if copied_parent = issues_map[issue.parent_id]
938 new_issue.parent_issue_id = copied_parent.id
946 new_issue.parent_issue_id = copied_parent.id
939 end
947 end
940 end
948 end
941
949
942 self.issues << new_issue
950 self.issues << new_issue
943 if new_issue.new_record?
951 if new_issue.new_record?
944 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
952 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
945 else
953 else
946 issues_map[issue.id] = new_issue unless new_issue.new_record?
954 issues_map[issue.id] = new_issue unless new_issue.new_record?
947 end
955 end
948 end
956 end
949
957
950 # Restore locked/closed version statuses
958 # Restore locked/closed version statuses
951 version_statuses.each do |version, status|
959 version_statuses.each do |version, status|
952 version.update_attribute :status, status
960 version.update_attribute :status, status
953 end
961 end
954
962
955 # Relations after in case issues related each other
963 # Relations after in case issues related each other
956 project.issues.each do |issue|
964 project.issues.each do |issue|
957 new_issue = issues_map[issue.id]
965 new_issue = issues_map[issue.id]
958 unless new_issue
966 unless new_issue
959 # Issue was not copied
967 # Issue was not copied
960 next
968 next
961 end
969 end
962
970
963 # Relations
971 # Relations
964 issue.relations_from.each do |source_relation|
972 issue.relations_from.each do |source_relation|
965 new_issue_relation = IssueRelation.new
973 new_issue_relation = IssueRelation.new
966 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
974 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
967 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
975 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
968 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
976 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
969 new_issue_relation.issue_to = source_relation.issue_to
977 new_issue_relation.issue_to = source_relation.issue_to
970 end
978 end
971 new_issue.relations_from << new_issue_relation
979 new_issue.relations_from << new_issue_relation
972 end
980 end
973
981
974 issue.relations_to.each do |source_relation|
982 issue.relations_to.each do |source_relation|
975 new_issue_relation = IssueRelation.new
983 new_issue_relation = IssueRelation.new
976 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
984 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
977 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
985 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
978 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
986 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
979 new_issue_relation.issue_from = source_relation.issue_from
987 new_issue_relation.issue_from = source_relation.issue_from
980 end
988 end
981 new_issue.relations_to << new_issue_relation
989 new_issue.relations_to << new_issue_relation
982 end
990 end
983 end
991 end
984 end
992 end
985
993
986 # Copies members from +project+
994 # Copies members from +project+
987 def copy_members(project)
995 def copy_members(project)
988 # Copy users first, then groups to handle members with inherited and given roles
996 # Copy users first, then groups to handle members with inherited and given roles
989 members_to_copy = []
997 members_to_copy = []
990 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
998 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
991 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
999 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
992
1000
993 members_to_copy.each do |member|
1001 members_to_copy.each do |member|
994 new_member = Member.new
1002 new_member = Member.new
995 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1003 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
996 # only copy non inherited roles
1004 # only copy non inherited roles
997 # inherited roles will be added when copying the group membership
1005 # inherited roles will be added when copying the group membership
998 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1006 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
999 next if role_ids.empty?
1007 next if role_ids.empty?
1000 new_member.role_ids = role_ids
1008 new_member.role_ids = role_ids
1001 new_member.project = self
1009 new_member.project = self
1002 self.members << new_member
1010 self.members << new_member
1003 end
1011 end
1004 end
1012 end
1005
1013
1006 # Copies queries from +project+
1014 # Copies queries from +project+
1007 def copy_queries(project)
1015 def copy_queries(project)
1008 project.queries.each do |query|
1016 project.queries.each do |query|
1009 new_query = IssueQuery.new
1017 new_query = IssueQuery.new
1010 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1018 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1011 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1019 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1012 new_query.project = self
1020 new_query.project = self
1013 new_query.user_id = query.user_id
1021 new_query.user_id = query.user_id
1014 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1022 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1015 self.queries << new_query
1023 self.queries << new_query
1016 end
1024 end
1017 end
1025 end
1018
1026
1019 # Copies boards from +project+
1027 # Copies boards from +project+
1020 def copy_boards(project)
1028 def copy_boards(project)
1021 project.boards.each do |board|
1029 project.boards.each do |board|
1022 new_board = Board.new
1030 new_board = Board.new
1023 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1031 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1024 new_board.project = self
1032 new_board.project = self
1025 self.boards << new_board
1033 self.boards << new_board
1026 end
1034 end
1027 end
1035 end
1028
1036
1029 def allowed_permissions
1037 def allowed_permissions
1030 @allowed_permissions ||= begin
1038 @allowed_permissions ||= begin
1031 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1039 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1032 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1040 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1033 end
1041 end
1034 end
1042 end
1035
1043
1036 def allowed_actions
1044 def allowed_actions
1037 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1045 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1038 end
1046 end
1039
1047
1040 # Archives subprojects recursively
1048 # Archives subprojects recursively
1041 def archive!
1049 def archive!
1042 children.each do |subproject|
1050 children.each do |subproject|
1043 subproject.send :archive!
1051 subproject.send :archive!
1044 end
1052 end
1045 update_attribute :status, STATUS_ARCHIVED
1053 update_attribute :status, STATUS_ARCHIVED
1046 end
1054 end
1047 end
1055 end
@@ -1,1039 +1,1039
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryCustomFieldColumn < QueryColumn
77 class QueryCustomFieldColumn < QueryColumn
78
78
79 def initialize(custom_field)
79 def initialize(custom_field)
80 self.name = "cf_#{custom_field.id}".to_sym
80 self.name = "cf_#{custom_field.id}".to_sym
81 self.sortable = custom_field.order_statement || false
81 self.sortable = custom_field.order_statement || false
82 self.groupable = custom_field.group_statement || false
82 self.groupable = custom_field.group_statement || false
83 self.totalable = custom_field.totalable?
83 self.totalable = custom_field.totalable?
84 @inline = true
84 @inline = true
85 @cf = custom_field
85 @cf = custom_field
86 end
86 end
87
87
88 def caption
88 def caption
89 @cf.name
89 @cf.name
90 end
90 end
91
91
92 def custom_field
92 def custom_field
93 @cf
93 @cf
94 end
94 end
95
95
96 def value_object(object)
96 def value_object(object)
97 if custom_field.visible_by?(object.project, User.current)
97 if custom_field.visible_by?(object.project, User.current)
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 else
100 else
101 nil
101 nil
102 end
102 end
103 end
103 end
104
104
105 def value(object)
105 def value(object)
106 raw = value_object(object)
106 raw = value_object(object)
107 if raw.is_a?(Array)
107 if raw.is_a?(Array)
108 raw.map {|r| @cf.cast_value(r.value)}
108 raw.map {|r| @cf.cast_value(r.value)}
109 elsif raw
109 elsif raw
110 @cf.cast_value(raw.value)
110 @cf.cast_value(raw.value)
111 else
111 else
112 nil
112 nil
113 end
113 end
114 end
114 end
115
115
116 def css_classes
116 def css_classes
117 @css_classes ||= "#{name} #{@cf.field_format}"
117 @css_classes ||= "#{name} #{@cf.field_format}"
118 end
118 end
119 end
119 end
120
120
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122
122
123 def initialize(association, custom_field)
123 def initialize(association, custom_field)
124 super(custom_field)
124 super(custom_field)
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 # TODO: support sorting/grouping by association custom field
126 # TODO: support sorting/grouping by association custom field
127 self.sortable = false
127 self.sortable = false
128 self.groupable = false
128 self.groupable = false
129 @association = association
129 @association = association
130 end
130 end
131
131
132 def value_object(object)
132 def value_object(object)
133 if assoc = object.send(@association)
133 if assoc = object.send(@association)
134 super(assoc)
134 super(assoc)
135 end
135 end
136 end
136 end
137
137
138 def css_classes
138 def css_classes
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 end
140 end
141 end
141 end
142
142
143 class Query < ActiveRecord::Base
143 class Query < ActiveRecord::Base
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 end
145 end
146
146
147 VISIBILITY_PRIVATE = 0
147 VISIBILITY_PRIVATE = 0
148 VISIBILITY_ROLES = 1
148 VISIBILITY_ROLES = 1
149 VISIBILITY_PUBLIC = 2
149 VISIBILITY_PUBLIC = 2
150
150
151 belongs_to :project
151 belongs_to :project
152 belongs_to :user
152 belongs_to :user
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
154 serialize :filters
154 serialize :filters
155 serialize :column_names
155 serialize :column_names
156 serialize :sort_criteria, Array
156 serialize :sort_criteria, Array
157 serialize :options, Hash
157 serialize :options, Hash
158
158
159 attr_protected :project_id, :user_id
159 attr_protected :project_id, :user_id
160
160
161 validates_presence_of :name
161 validates_presence_of :name
162 validates_length_of :name, :maximum => 255
162 validates_length_of :name, :maximum => 255
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
164 validate :validate_query_filters
164 validate :validate_query_filters
165 validate do |query|
165 validate do |query|
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
167 end
167 end
168
168
169 after_save do |query|
169 after_save do |query|
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
171 query.roles.clear
171 query.roles.clear
172 end
172 end
173 end
173 end
174
174
175 class_attribute :operators
175 class_attribute :operators
176 self.operators = {
176 self.operators = {
177 "=" => :label_equals,
177 "=" => :label_equals,
178 "!" => :label_not_equals,
178 "!" => :label_not_equals,
179 "o" => :label_open_issues,
179 "o" => :label_open_issues,
180 "c" => :label_closed_issues,
180 "c" => :label_closed_issues,
181 "!*" => :label_none,
181 "!*" => :label_none,
182 "*" => :label_any,
182 "*" => :label_any,
183 ">=" => :label_greater_or_equal,
183 ">=" => :label_greater_or_equal,
184 "<=" => :label_less_or_equal,
184 "<=" => :label_less_or_equal,
185 "><" => :label_between,
185 "><" => :label_between,
186 "<t+" => :label_in_less_than,
186 "<t+" => :label_in_less_than,
187 ">t+" => :label_in_more_than,
187 ">t+" => :label_in_more_than,
188 "><t+"=> :label_in_the_next_days,
188 "><t+"=> :label_in_the_next_days,
189 "t+" => :label_in,
189 "t+" => :label_in,
190 "t" => :label_today,
190 "t" => :label_today,
191 "ld" => :label_yesterday,
191 "ld" => :label_yesterday,
192 "w" => :label_this_week,
192 "w" => :label_this_week,
193 "lw" => :label_last_week,
193 "lw" => :label_last_week,
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
195 "m" => :label_this_month,
195 "m" => :label_this_month,
196 "lm" => :label_last_month,
196 "lm" => :label_last_month,
197 "y" => :label_this_year,
197 "y" => :label_this_year,
198 ">t-" => :label_less_than_ago,
198 ">t-" => :label_less_than_ago,
199 "<t-" => :label_more_than_ago,
199 "<t-" => :label_more_than_ago,
200 "><t-"=> :label_in_the_past_days,
200 "><t-"=> :label_in_the_past_days,
201 "t-" => :label_ago,
201 "t-" => :label_ago,
202 "~" => :label_contains,
202 "~" => :label_contains,
203 "!~" => :label_not_contains,
203 "!~" => :label_not_contains,
204 "=p" => :label_any_issues_in_project,
204 "=p" => :label_any_issues_in_project,
205 "=!p" => :label_any_issues_not_in_project,
205 "=!p" => :label_any_issues_not_in_project,
206 "!p" => :label_no_issues_in_project,
206 "!p" => :label_no_issues_in_project,
207 "*o" => :label_any_open_issues,
207 "*o" => :label_any_open_issues,
208 "!o" => :label_no_open_issues
208 "!o" => :label_no_open_issues
209 }
209 }
210
210
211 class_attribute :operators_by_filter_type
211 class_attribute :operators_by_filter_type
212 self.operators_by_filter_type = {
212 self.operators_by_filter_type = {
213 :list => [ "=", "!" ],
213 :list => [ "=", "!" ],
214 :list_status => [ "o", "=", "!", "c", "*" ],
214 :list_status => [ "o", "=", "!", "c", "*" ],
215 :list_optional => [ "=", "!", "!*", "*" ],
215 :list_optional => [ "=", "!", "!*", "*" ],
216 :list_subprojects => [ "*", "!*", "=" ],
216 :list_subprojects => [ "*", "!*", "=" ],
217 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
217 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
218 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
218 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
219 :string => [ "=", "~", "!", "!~", "!*", "*" ],
219 :string => [ "=", "~", "!", "!~", "!*", "*" ],
220 :text => [ "~", "!~", "!*", "*" ],
220 :text => [ "~", "!~", "!*", "*" ],
221 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
221 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
222 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
222 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
223 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
223 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
224 :tree => ["=", "~", "!*", "*"]
224 :tree => ["=", "~", "!*", "*"]
225 }
225 }
226
226
227 class_attribute :available_columns
227 class_attribute :available_columns
228 self.available_columns = []
228 self.available_columns = []
229
229
230 class_attribute :queried_class
230 class_attribute :queried_class
231
231
232 def queried_table_name
232 def queried_table_name
233 @queried_table_name ||= self.class.queried_class.table_name
233 @queried_table_name ||= self.class.queried_class.table_name
234 end
234 end
235
235
236 def initialize(attributes=nil, *args)
236 def initialize(attributes=nil, *args)
237 super attributes
237 super attributes
238 @is_for_all = project.nil?
238 @is_for_all = project.nil?
239 end
239 end
240
240
241 # Builds the query from the given params
241 # Builds the query from the given params
242 def build_from_params(params)
242 def build_from_params(params)
243 if params[:fields] || params[:f]
243 if params[:fields] || params[:f]
244 self.filters = {}
244 self.filters = {}
245 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
245 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
246 else
246 else
247 available_filters.keys.each do |field|
247 available_filters.keys.each do |field|
248 add_short_filter(field, params[field]) if params[field]
248 add_short_filter(field, params[field]) if params[field]
249 end
249 end
250 end
250 end
251 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
251 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
252 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
252 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
253 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
253 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
254 self
254 self
255 end
255 end
256
256
257 # Builds a new query from the given params and attributes
257 # Builds a new query from the given params and attributes
258 def self.build_from_params(params, attributes={})
258 def self.build_from_params(params, attributes={})
259 new(attributes).build_from_params(params)
259 new(attributes).build_from_params(params)
260 end
260 end
261
261
262 def validate_query_filters
262 def validate_query_filters
263 filters.each_key do |field|
263 filters.each_key do |field|
264 if values_for(field)
264 if values_for(field)
265 case type_for(field)
265 case type_for(field)
266 when :integer
266 when :integer
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
268 when :float
268 when :float
269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
270 when :date, :date_past
270 when :date, :date_past
271 case operator_for(field)
271 case operator_for(field)
272 when "=", ">=", "<=", "><"
272 when "=", ">=", "<=", "><"
273 add_filter_error(field, :invalid) if values_for(field).detect {|v|
273 add_filter_error(field, :invalid) if values_for(field).detect {|v|
274 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
274 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
275 }
275 }
276 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
276 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
277 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
277 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
278 end
278 end
279 end
279 end
280 end
280 end
281
281
282 add_filter_error(field, :blank) unless
282 add_filter_error(field, :blank) unless
283 # filter requires one or more values
283 # filter requires one or more values
284 (values_for(field) and !values_for(field).first.blank?) or
284 (values_for(field) and !values_for(field).first.blank?) or
285 # filter doesn't require any value
285 # filter doesn't require any value
286 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
286 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
287 end if filters
287 end if filters
288 end
288 end
289
289
290 def add_filter_error(field, message)
290 def add_filter_error(field, message)
291 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
291 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
292 errors.add(:base, m)
292 errors.add(:base, m)
293 end
293 end
294
294
295 def editable_by?(user)
295 def editable_by?(user)
296 return false unless user
296 return false unless user
297 # Admin can edit them all and regular users can edit their private queries
297 # Admin can edit them all and regular users can edit their private queries
298 return true if user.admin? || (is_private? && self.user_id == user.id)
298 return true if user.admin? || (is_private? && self.user_id == user.id)
299 # Members can not edit public queries that are for all project (only admin is allowed to)
299 # Members can not edit public queries that are for all project (only admin is allowed to)
300 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
300 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
301 end
301 end
302
302
303 def trackers
303 def trackers
304 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
304 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
305 end
305 end
306
306
307 # Returns a hash of localized labels for all filter operators
307 # Returns a hash of localized labels for all filter operators
308 def self.operators_labels
308 def self.operators_labels
309 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
309 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
310 end
310 end
311
311
312 # Returns a representation of the available filters for JSON serialization
312 # Returns a representation of the available filters for JSON serialization
313 def available_filters_as_json
313 def available_filters_as_json
314 json = {}
314 json = {}
315 available_filters.each do |field, options|
315 available_filters.each do |field, options|
316 options = options.slice(:type, :name, :values)
316 options = options.slice(:type, :name, :values)
317 if options[:values] && values_for(field)
317 if options[:values] && values_for(field)
318 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
318 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
319 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
319 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
320 options[:values] += send(method, missing)
320 options[:values] += send(method, missing)
321 end
321 end
322 end
322 end
323 json[field] = options.stringify_keys
323 json[field] = options.stringify_keys
324 end
324 end
325 json
325 json
326 end
326 end
327
327
328 def all_projects
328 def all_projects
329 @all_projects ||= Project.visible.to_a
329 @all_projects ||= Project.visible.to_a
330 end
330 end
331
331
332 def all_projects_values
332 def all_projects_values
333 return @all_projects_values if @all_projects_values
333 return @all_projects_values if @all_projects_values
334
334
335 values = []
335 values = []
336 Project.project_tree(all_projects) do |p, level|
336 Project.project_tree(all_projects) do |p, level|
337 prefix = (level > 0 ? ('--' * level + ' ') : '')
337 prefix = (level > 0 ? ('--' * level + ' ') : '')
338 values << ["#{prefix}#{p.name}", p.id.to_s]
338 values << ["#{prefix}#{p.name}", p.id.to_s]
339 end
339 end
340 @all_projects_values = values
340 @all_projects_values = values
341 end
341 end
342
342
343 # Adds available filters
343 # Adds available filters
344 def initialize_available_filters
344 def initialize_available_filters
345 # implemented by sub-classes
345 # implemented by sub-classes
346 end
346 end
347 protected :initialize_available_filters
347 protected :initialize_available_filters
348
348
349 # Adds an available filter
349 # Adds an available filter
350 def add_available_filter(field, options)
350 def add_available_filter(field, options)
351 @available_filters ||= ActiveSupport::OrderedHash.new
351 @available_filters ||= ActiveSupport::OrderedHash.new
352 @available_filters[field] = options
352 @available_filters[field] = options
353 @available_filters
353 @available_filters
354 end
354 end
355
355
356 # Removes an available filter
356 # Removes an available filter
357 def delete_available_filter(field)
357 def delete_available_filter(field)
358 if @available_filters
358 if @available_filters
359 @available_filters.delete(field)
359 @available_filters.delete(field)
360 end
360 end
361 end
361 end
362
362
363 # Return a hash of available filters
363 # Return a hash of available filters
364 def available_filters
364 def available_filters
365 unless @available_filters
365 unless @available_filters
366 initialize_available_filters
366 initialize_available_filters
367 @available_filters.each do |field, options|
367 @available_filters.each do |field, options|
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
369 end
369 end
370 end
370 end
371 @available_filters
371 @available_filters
372 end
372 end
373
373
374 def add_filter(field, operator, values=nil)
374 def add_filter(field, operator, values=nil)
375 # values must be an array
375 # values must be an array
376 return unless values.nil? || values.is_a?(Array)
376 return unless values.nil? || values.is_a?(Array)
377 # check if field is defined as an available filter
377 # check if field is defined as an available filter
378 if available_filters.has_key? field
378 if available_filters.has_key? field
379 filter_options = available_filters[field]
379 filter_options = available_filters[field]
380 filters[field] = {:operator => operator, :values => (values || [''])}
380 filters[field] = {:operator => operator, :values => (values || [''])}
381 end
381 end
382 end
382 end
383
383
384 def add_short_filter(field, expression)
384 def add_short_filter(field, expression)
385 return unless expression && available_filters.has_key?(field)
385 return unless expression && available_filters.has_key?(field)
386 field_type = available_filters[field][:type]
386 field_type = available_filters[field][:type]
387 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
387 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
388 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
388 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
389 values = $1
389 values = $1
390 add_filter field, operator, values.present? ? values.split('|') : ['']
390 add_filter field, operator, values.present? ? values.split('|') : ['']
391 end || add_filter(field, '=', expression.split('|'))
391 end || add_filter(field, '=', expression.split('|'))
392 end
392 end
393
393
394 # Add multiple filters using +add_filter+
394 # Add multiple filters using +add_filter+
395 def add_filters(fields, operators, values)
395 def add_filters(fields, operators, values)
396 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
396 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
397 fields.each do |field|
397 fields.each do |field|
398 add_filter(field, operators[field], values && values[field])
398 add_filter(field, operators[field], values && values[field])
399 end
399 end
400 end
400 end
401 end
401 end
402
402
403 def has_filter?(field)
403 def has_filter?(field)
404 filters and filters[field]
404 filters and filters[field]
405 end
405 end
406
406
407 def type_for(field)
407 def type_for(field)
408 available_filters[field][:type] if available_filters.has_key?(field)
408 available_filters[field][:type] if available_filters.has_key?(field)
409 end
409 end
410
410
411 def operator_for(field)
411 def operator_for(field)
412 has_filter?(field) ? filters[field][:operator] : nil
412 has_filter?(field) ? filters[field][:operator] : nil
413 end
413 end
414
414
415 def values_for(field)
415 def values_for(field)
416 has_filter?(field) ? filters[field][:values] : nil
416 has_filter?(field) ? filters[field][:values] : nil
417 end
417 end
418
418
419 def value_for(field, index=0)
419 def value_for(field, index=0)
420 (values_for(field) || [])[index]
420 (values_for(field) || [])[index]
421 end
421 end
422
422
423 def label_for(field)
423 def label_for(field)
424 label = available_filters[field][:name] if available_filters.has_key?(field)
424 label = available_filters[field][:name] if available_filters.has_key?(field)
425 label ||= queried_class.human_attribute_name(field, :default => field)
425 label ||= queried_class.human_attribute_name(field, :default => field)
426 end
426 end
427
427
428 def self.add_available_column(column)
428 def self.add_available_column(column)
429 self.available_columns << (column) if column.is_a?(QueryColumn)
429 self.available_columns << (column) if column.is_a?(QueryColumn)
430 end
430 end
431
431
432 # Returns an array of columns that can be used to group the results
432 # Returns an array of columns that can be used to group the results
433 def groupable_columns
433 def groupable_columns
434 available_columns.select {|c| c.groupable}
434 available_columns.select {|c| c.groupable}
435 end
435 end
436
436
437 # Returns a Hash of columns and the key for sorting
437 # Returns a Hash of columns and the key for sorting
438 def sortable_columns
438 def sortable_columns
439 available_columns.inject({}) {|h, column|
439 available_columns.inject({}) {|h, column|
440 h[column.name.to_s] = column.sortable
440 h[column.name.to_s] = column.sortable
441 h
441 h
442 }
442 }
443 end
443 end
444
444
445 def columns
445 def columns
446 # preserve the column_names order
446 # preserve the column_names order
447 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
447 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
448 available_columns.find { |col| col.name == name }
448 available_columns.find { |col| col.name == name }
449 end.compact
449 end.compact
450 available_columns.select(&:frozen?) | cols
450 available_columns.select(&:frozen?) | cols
451 end
451 end
452
452
453 def inline_columns
453 def inline_columns
454 columns.select(&:inline?)
454 columns.select(&:inline?)
455 end
455 end
456
456
457 def block_columns
457 def block_columns
458 columns.reject(&:inline?)
458 columns.reject(&:inline?)
459 end
459 end
460
460
461 def available_inline_columns
461 def available_inline_columns
462 available_columns.select(&:inline?)
462 available_columns.select(&:inline?)
463 end
463 end
464
464
465 def available_block_columns
465 def available_block_columns
466 available_columns.reject(&:inline?)
466 available_columns.reject(&:inline?)
467 end
467 end
468
468
469 def available_totalable_columns
469 def available_totalable_columns
470 available_columns.select(&:totalable)
470 available_columns.select(&:totalable)
471 end
471 end
472
472
473 def default_columns_names
473 def default_columns_names
474 []
474 []
475 end
475 end
476
476
477 def column_names=(names)
477 def column_names=(names)
478 if names
478 if names
479 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
479 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
480 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
480 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
481 # Set column_names to nil if default columns
481 # Set column_names to nil if default columns
482 if names == default_columns_names
482 if names == default_columns_names
483 names = nil
483 names = nil
484 end
484 end
485 end
485 end
486 write_attribute(:column_names, names)
486 write_attribute(:column_names, names)
487 end
487 end
488
488
489 def has_column?(column)
489 def has_column?(column)
490 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
490 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
491 end
491 end
492
492
493 def has_custom_field_column?
493 def has_custom_field_column?
494 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
494 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
495 end
495 end
496
496
497 def has_default_columns?
497 def has_default_columns?
498 column_names.nil? || column_names.empty?
498 column_names.nil? || column_names.empty?
499 end
499 end
500
500
501 def totalable_columns
501 def totalable_columns
502 names = totalable_names
502 names = totalable_names
503 available_totalable_columns.select {|column| names.include?(column.name)}
503 available_totalable_columns.select {|column| names.include?(column.name)}
504 end
504 end
505
505
506 def totalable_names=(names)
506 def totalable_names=(names)
507 if names
507 if names
508 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
508 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
509 end
509 end
510 options[:totalable_names] = names
510 options[:totalable_names] = names
511 end
511 end
512
512
513 def totalable_names
513 def totalable_names
514 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
514 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
515 end
515 end
516
516
517 def sort_criteria=(arg)
517 def sort_criteria=(arg)
518 c = []
518 c = []
519 if arg.is_a?(Hash)
519 if arg.is_a?(Hash)
520 arg = arg.keys.sort.collect {|k| arg[k]}
520 arg = arg.keys.sort.collect {|k| arg[k]}
521 end
521 end
522 if arg
522 if arg
523 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
523 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
524 end
524 end
525 write_attribute(:sort_criteria, c)
525 write_attribute(:sort_criteria, c)
526 end
526 end
527
527
528 def sort_criteria
528 def sort_criteria
529 read_attribute(:sort_criteria) || []
529 read_attribute(:sort_criteria) || []
530 end
530 end
531
531
532 def sort_criteria_key(arg)
532 def sort_criteria_key(arg)
533 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
533 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
534 end
534 end
535
535
536 def sort_criteria_order(arg)
536 def sort_criteria_order(arg)
537 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
537 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
538 end
538 end
539
539
540 def sort_criteria_order_for(key)
540 def sort_criteria_order_for(key)
541 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
541 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
542 end
542 end
543
543
544 # Returns the SQL sort order that should be prepended for grouping
544 # Returns the SQL sort order that should be prepended for grouping
545 def group_by_sort_order
545 def group_by_sort_order
546 if column = group_by_column
546 if column = group_by_column
547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
548 Array(column.sortable).map {|s| "#{s} #{order}"}
548 Array(column.sortable).map {|s| "#{s} #{order}"}
549 end
549 end
550 end
550 end
551
551
552 # Returns true if the query is a grouped query
552 # Returns true if the query is a grouped query
553 def grouped?
553 def grouped?
554 !group_by_column.nil?
554 !group_by_column.nil?
555 end
555 end
556
556
557 def group_by_column
557 def group_by_column
558 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
558 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
559 end
559 end
560
560
561 def group_by_statement
561 def group_by_statement
562 group_by_column.try(:groupable)
562 group_by_column.try(:groupable)
563 end
563 end
564
564
565 def project_statement
565 def project_statement
566 project_clauses = []
566 project_clauses = []
567 if project && !project.descendants.active.empty?
567 if project && !project.descendants.active.empty?
568 if has_filter?("subproject_id")
568 if has_filter?("subproject_id")
569 case operator_for("subproject_id")
569 case operator_for("subproject_id")
570 when '='
570 when '='
571 # include the selected subprojects
571 # include the selected subprojects
572 ids = [project.id] + values_for("subproject_id").each(&:to_i)
572 ids = [project.id] + values_for("subproject_id").each(&:to_i)
573 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
573 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
574 when '!*'
574 when '!*'
575 # main project only
575 # main project only
576 project_clauses << "#{Project.table_name}.id = %d" % project.id
576 project_clauses << "#{Project.table_name}.id = %d" % project.id
577 else
577 else
578 # all subprojects
578 # all subprojects
579 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
579 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
580 end
580 end
581 elsif Setting.display_subprojects_issues?
581 elsif Setting.display_subprojects_issues?
582 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
582 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
583 else
583 else
584 project_clauses << "#{Project.table_name}.id = %d" % project.id
584 project_clauses << "#{Project.table_name}.id = %d" % project.id
585 end
585 end
586 elsif project
586 elsif project
587 project_clauses << "#{Project.table_name}.id = %d" % project.id
587 project_clauses << "#{Project.table_name}.id = %d" % project.id
588 end
588 end
589 project_clauses.any? ? project_clauses.join(' AND ') : nil
589 project_clauses.any? ? project_clauses.join(' AND ') : nil
590 end
590 end
591
591
592 def statement
592 def statement
593 # filters clauses
593 # filters clauses
594 filters_clauses = []
594 filters_clauses = []
595 filters.each_key do |field|
595 filters.each_key do |field|
596 next if field == "subproject_id"
596 next if field == "subproject_id"
597 v = values_for(field).clone
597 v = values_for(field).clone
598 next unless v and !v.empty?
598 next unless v and !v.empty?
599 operator = operator_for(field)
599 operator = operator_for(field)
600
600
601 # "me" value substitution
601 # "me" value substitution
602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
603 if v.delete("me")
603 if v.delete("me")
604 if User.current.logged?
604 if User.current.logged?
605 v.push(User.current.id.to_s)
605 v.push(User.current.id.to_s)
606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
607 else
607 else
608 v.push("0")
608 v.push("0")
609 end
609 end
610 end
610 end
611 end
611 end
612
612
613 if field == 'project_id'
613 if field == 'project_id'
614 if v.delete('mine')
614 if v.delete('mine')
615 v += User.current.memberships.map(&:project_id).map(&:to_s)
615 v += User.current.memberships.map(&:project_id).map(&:to_s)
616 end
616 end
617 end
617 end
618
618
619 if field =~ /cf_(\d+)$/
619 if field =~ /cf_(\d+)$/
620 # custom field
620 # custom field
621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
622 elsif respond_to?("sql_for_#{field}_field")
622 elsif respond_to?("sql_for_#{field}_field")
623 # specific statement
623 # specific statement
624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
625 else
625 else
626 # regular field
626 # regular field
627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
628 end
628 end
629 end if filters and valid?
629 end if filters and valid?
630
630
631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
632 # Excludes results for which the grouped custom field is not visible
632 # Excludes results for which the grouped custom field is not visible
633 filters_clauses << c.custom_field.visibility_by_project_condition
633 filters_clauses << c.custom_field.visibility_by_project_condition
634 end
634 end
635
635
636 filters_clauses << project_statement
636 filters_clauses << project_statement
637 filters_clauses.reject!(&:blank?)
637 filters_clauses.reject!(&:blank?)
638
638
639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
640 end
640 end
641
641
642 # Returns the sum of values for the given column
642 # Returns the sum of values for the given column
643 def total_for(column)
643 def total_for(column)
644 total_with_scope(column, base_scope)
644 total_with_scope(column, base_scope)
645 end
645 end
646
646
647 # Returns a hash of the sum of the given column for each group,
647 # Returns a hash of the sum of the given column for each group,
648 # or nil if the query is not grouped
648 # or nil if the query is not grouped
649 def total_by_group_for(column)
649 def total_by_group_for(column)
650 grouped_query do |scope|
650 grouped_query do |scope|
651 total_with_scope(column, scope)
651 total_with_scope(column, scope)
652 end
652 end
653 end
653 end
654
654
655 def totals
655 def totals
656 totals = totalable_columns.map {|column| [column, total_for(column)]}
656 totals = totalable_columns.map {|column| [column, total_for(column)]}
657 yield totals if block_given?
657 yield totals if block_given?
658 totals
658 totals
659 end
659 end
660
660
661 def totals_by_group
661 def totals_by_group
662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
663 yield totals if block_given?
663 yield totals if block_given?
664 totals
664 totals
665 end
665 end
666
666
667 private
667 private
668
668
669 def grouped_query(&block)
669 def grouped_query(&block)
670 r = nil
670 r = nil
671 if grouped?
671 if grouped?
672 begin
672 begin
673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
674 r = yield base_group_scope
674 r = yield base_group_scope
675 rescue ActiveRecord::RecordNotFound
675 rescue ActiveRecord::RecordNotFound
676 r = {nil => yield(base_scope)}
676 r = {nil => yield(base_scope)}
677 end
677 end
678 c = group_by_column
678 c = group_by_column
679 if c.is_a?(QueryCustomFieldColumn)
679 if c.is_a?(QueryCustomFieldColumn)
680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
681 end
681 end
682 end
682 end
683 r
683 r
684 rescue ::ActiveRecord::StatementInvalid => e
684 rescue ::ActiveRecord::StatementInvalid => e
685 raise StatementInvalid.new(e.message)
685 raise StatementInvalid.new(e.message)
686 end
686 end
687
687
688 def total_with_scope(column, scope)
688 def total_with_scope(column, scope)
689 unless column.is_a?(QueryColumn)
689 unless column.is_a?(QueryColumn)
690 column = column.to_sym
690 column = column.to_sym
691 column = available_totalable_columns.detect {|c| c.name == column}
691 column = available_totalable_columns.detect {|c| c.name == column}
692 end
692 end
693 if column.is_a?(QueryCustomFieldColumn)
693 if column.is_a?(QueryCustomFieldColumn)
694 custom_field = column.custom_field
694 custom_field = column.custom_field
695 send "total_for_custom_field", custom_field, scope
695 send "total_for_custom_field", custom_field, scope
696 else
696 else
697 send "total_for_#{column.name}", scope
697 send "total_for_#{column.name}", scope
698 end
698 end
699 rescue ::ActiveRecord::StatementInvalid => e
699 rescue ::ActiveRecord::StatementInvalid => e
700 raise StatementInvalid.new(e.message)
700 raise StatementInvalid.new(e.message)
701 end
701 end
702
702
703 def base_scope
703 def base_scope
704 raise "unimplemented"
704 raise "unimplemented"
705 end
705 end
706
706
707 def base_group_scope
707 def base_group_scope
708 base_scope.
708 base_scope.
709 joins(joins_for_order_statement(group_by_statement)).
709 joins(joins_for_order_statement(group_by_statement)).
710 group(group_by_statement)
710 group(group_by_statement)
711 end
711 end
712
712
713 def total_for_custom_field(custom_field, scope, &block)
713 def total_for_custom_field(custom_field, scope, &block)
714 total = custom_field.format.total_for_scope(custom_field, scope)
714 total = custom_field.format.total_for_scope(custom_field, scope)
715 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
715 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
716 total
716 total
717 end
717 end
718
718
719 def map_total(total, &block)
719 def map_total(total, &block)
720 if total.is_a?(Hash)
720 if total.is_a?(Hash)
721 total.keys.each {|k| total[k] = yield total[k]}
721 total.keys.each {|k| total[k] = yield total[k]}
722 else
722 else
723 total = yield total
723 total = yield total
724 end
724 end
725 total
725 total
726 end
726 end
727
727
728 def sql_for_custom_field(field, operator, value, custom_field_id)
728 def sql_for_custom_field(field, operator, value, custom_field_id)
729 db_table = CustomValue.table_name
729 db_table = CustomValue.table_name
730 db_field = 'value'
730 db_field = 'value'
731 filter = @available_filters[field]
731 filter = @available_filters[field]
732 return nil unless filter
732 return nil unless filter
733 if filter[:field].format.target_class && filter[:field].format.target_class <= User
733 if filter[:field].format.target_class && filter[:field].format.target_class <= User
734 if value.delete('me')
734 if value.delete('me')
735 value.push User.current.id.to_s
735 value.push User.current.id.to_s
736 end
736 end
737 end
737 end
738 not_in = nil
738 not_in = nil
739 if operator == '!'
739 if operator == '!'
740 # Makes ! operator work for custom fields with multiple values
740 # Makes ! operator work for custom fields with multiple values
741 operator = '='
741 operator = '='
742 not_in = 'NOT'
742 not_in = 'NOT'
743 end
743 end
744 customized_key = "id"
744 customized_key = "id"
745 customized_class = queried_class
745 customized_class = queried_class
746 if field =~ /^(.+)\.cf_/
746 if field =~ /^(.+)\.cf_/
747 assoc = $1
747 assoc = $1
748 customized_key = "#{assoc}_id"
748 customized_key = "#{assoc}_id"
749 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
749 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
750 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
750 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
751 end
751 end
752 where = sql_for_field(field, operator, value, db_table, db_field, true)
752 where = sql_for_field(field, operator, value, db_table, db_field, true)
753 if operator =~ /[<>]/
753 if operator =~ /[<>]/
754 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
754 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
755 end
755 end
756 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
756 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
757 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
757 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
758 " 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}" +
758 " 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}" +
759 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
759 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
760 end
760 end
761
761
762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
764 sql = ''
764 sql = ''
765 case operator
765 case operator
766 when "="
766 when "="
767 if value.any?
767 if value.any?
768 case type_for(field)
768 case type_for(field)
769 when :date, :date_past
769 when :date, :date_past
770 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
770 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
771 when :integer
771 when :integer
772 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
772 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
773 if int_values.present?
773 if int_values.present?
774 if is_custom_filter
774 if is_custom_filter
775 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
775 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
776 else
776 else
777 sql = "#{db_table}.#{db_field} IN (#{int_values})"
777 sql = "#{db_table}.#{db_field} IN (#{int_values})"
778 end
778 end
779 else
779 else
780 sql = "1=0"
780 sql = "1=0"
781 end
781 end
782 when :float
782 when :float
783 if is_custom_filter
783 if is_custom_filter
784 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
784 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
785 else
785 else
786 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
786 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
787 end
787 end
788 else
788 else
789 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
789 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
790 end
790 end
791 else
791 else
792 # IN an empty set
792 # IN an empty set
793 sql = "1=0"
793 sql = "1=0"
794 end
794 end
795 when "!"
795 when "!"
796 if value.any?
796 if value.any?
797 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
797 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
798 else
798 else
799 # NOT IN an empty set
799 # NOT IN an empty set
800 sql = "1=1"
800 sql = "1=1"
801 end
801 end
802 when "!*"
802 when "!*"
803 sql = "#{db_table}.#{db_field} IS NULL"
803 sql = "#{db_table}.#{db_field} IS NULL"
804 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
804 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
805 when "*"
805 when "*"
806 sql = "#{db_table}.#{db_field} IS NOT NULL"
806 sql = "#{db_table}.#{db_field} IS NOT NULL"
807 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
807 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
808 when ">="
808 when ">="
809 if [:date, :date_past].include?(type_for(field))
809 if [:date, :date_past].include?(type_for(field))
810 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
810 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
811 else
811 else
812 if is_custom_filter
812 if is_custom_filter
813 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
813 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
814 else
814 else
815 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
815 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
816 end
816 end
817 end
817 end
818 when "<="
818 when "<="
819 if [:date, :date_past].include?(type_for(field))
819 if [:date, :date_past].include?(type_for(field))
820 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
820 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
821 else
821 else
822 if is_custom_filter
822 if is_custom_filter
823 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
823 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
824 else
824 else
825 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
825 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
826 end
826 end
827 end
827 end
828 when "><"
828 when "><"
829 if [:date, :date_past].include?(type_for(field))
829 if [:date, :date_past].include?(type_for(field))
830 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
830 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
831 else
831 else
832 if is_custom_filter
832 if is_custom_filter
833 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
833 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
834 else
834 else
835 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
835 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
836 end
836 end
837 end
837 end
838 when "o"
838 when "o"
839 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
839 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
840 when "c"
840 when "c"
841 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
841 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
842 when "><t-"
842 when "><t-"
843 # between today - n days and today
843 # between today - n days and today
844 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
844 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
845 when ">t-"
845 when ">t-"
846 # >= today - n days
846 # >= today - n days
847 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
847 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
848 when "<t-"
848 when "<t-"
849 # <= today - n days
849 # <= today - n days
850 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
850 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
851 when "t-"
851 when "t-"
852 # = n days in past
852 # = n days in past
853 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
853 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
854 when "><t+"
854 when "><t+"
855 # between today and today + n days
855 # between today and today + n days
856 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
856 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
857 when ">t+"
857 when ">t+"
858 # >= today + n days
858 # >= today + n days
859 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
859 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
860 when "<t+"
860 when "<t+"
861 # <= today + n days
861 # <= today + n days
862 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
862 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
863 when "t+"
863 when "t+"
864 # = today + n days
864 # = today + n days
865 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
865 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
866 when "t"
866 when "t"
867 # = today
867 # = today
868 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
868 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
869 when "ld"
869 when "ld"
870 # = yesterday
870 # = yesterday
871 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
871 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
872 when "w"
872 when "w"
873 # = this week
873 # = this week
874 first_day_of_week = l(:general_first_day_of_week).to_i
874 first_day_of_week = l(:general_first_day_of_week).to_i
875 day_of_week = User.current.today.cwday
875 day_of_week = User.current.today.cwday
876 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
876 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
877 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
877 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
878 when "lw"
878 when "lw"
879 # = last week
879 # = last week
880 first_day_of_week = l(:general_first_day_of_week).to_i
880 first_day_of_week = l(:general_first_day_of_week).to_i
881 day_of_week = User.current.today.cwday
881 day_of_week = User.current.today.cwday
882 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
882 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
883 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
883 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
884 when "l2w"
884 when "l2w"
885 # = last 2 weeks
885 # = last 2 weeks
886 first_day_of_week = l(:general_first_day_of_week).to_i
886 first_day_of_week = l(:general_first_day_of_week).to_i
887 day_of_week = User.current.today.cwday
887 day_of_week = User.current.today.cwday
888 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
888 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
889 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
889 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
890 when "m"
890 when "m"
891 # = this month
891 # = this month
892 date = User.current.today
892 date = User.current.today
893 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
893 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
894 when "lm"
894 when "lm"
895 # = last month
895 # = last month
896 date = User.current.today.prev_month
896 date = User.current.today.prev_month
897 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
897 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
898 when "y"
898 when "y"
899 # = this year
899 # = this year
900 date = User.current.today
900 date = User.current.today
901 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
901 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
902 when "~"
902 when "~"
903 sql = sql_contains("#{db_table}.#{db_field}", value.first)
903 sql = sql_contains("#{db_table}.#{db_field}", value.first)
904 when "!~"
904 when "!~"
905 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
905 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
906 else
906 else
907 raise "Unknown query operator #{operator}"
907 raise "Unknown query operator #{operator}"
908 end
908 end
909
909
910 return sql
910 return sql
911 end
911 end
912
912
913 # Returns a SQL LIKE statement with wildcards
913 # Returns a SQL LIKE statement with wildcards
914 def sql_contains(db_field, value, match=true)
914 def sql_contains(db_field, value, match=true)
915 queried_class.send :sanitize_sql_for_conditions,
915 queried_class.send :sanitize_sql_for_conditions,
916 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
916 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
917 end
917 end
918
918
919 # Adds a filter for the given custom field
919 # Adds a filter for the given custom field
920 def add_custom_field_filter(field, assoc=nil)
920 def add_custom_field_filter(field, assoc=nil)
921 options = field.query_filter_options(self)
921 options = field.query_filter_options(self)
922 if field.format.target_class && field.format.target_class <= User
922 if field.format.target_class && field.format.target_class <= User
923 if options[:values].is_a?(Array) && User.current.logged?
923 if options[:values].is_a?(Array) && User.current.logged?
924 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
924 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
925 end
925 end
926 end
926 end
927
927
928 filter_id = "cf_#{field.id}"
928 filter_id = "cf_#{field.id}"
929 filter_name = field.name
929 filter_name = field.name
930 if assoc.present?
930 if assoc.present?
931 filter_id = "#{assoc}.#{filter_id}"
931 filter_id = "#{assoc}.#{filter_id}"
932 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
932 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
933 end
933 end
934 add_available_filter filter_id, options.merge({
934 add_available_filter filter_id, options.merge({
935 :name => filter_name,
935 :name => filter_name,
936 :field => field
936 :field => field
937 })
937 })
938 end
938 end
939
939
940 # Adds filters for the given custom fields scope
940 # Adds filters for the given custom fields scope
941 def add_custom_fields_filters(scope, assoc=nil)
941 def add_custom_fields_filters(scope, assoc=nil)
942 scope.visible.where(:is_filter => true).sorted.each do |field|
942 scope.visible.where(:is_filter => true).sorted.each do |field|
943 add_custom_field_filter(field, assoc)
943 add_custom_field_filter(field, assoc)
944 end
944 end
945 end
945 end
946
946
947 # Adds filters for the given associations custom fields
947 # Adds filters for the given associations custom fields
948 def add_associations_custom_fields_filters(*associations)
948 def add_associations_custom_fields_filters(*associations)
949 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
949 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
950 associations.each do |assoc|
950 associations.each do |assoc|
951 association_klass = queried_class.reflect_on_association(assoc).klass
951 association_klass = queried_class.reflect_on_association(assoc).klass
952 fields_by_class.each do |field_class, fields|
952 fields_by_class.each do |field_class, fields|
953 if field_class.customized_class <= association_klass
953 if field_class.customized_class <= association_klass
954 fields.sort.each do |field|
954 fields.sort.each do |field|
955 add_custom_field_filter(field, assoc)
955 add_custom_field_filter(field, assoc)
956 end
956 end
957 end
957 end
958 end
958 end
959 end
959 end
960 end
960 end
961
961
962 def quoted_time(time, is_custom_filter)
962 def quoted_time(time, is_custom_filter)
963 if is_custom_filter
963 if is_custom_filter
964 # Custom field values are stored as strings in the DB
964 # Custom field values are stored as strings in the DB
965 # using this format that does not depend on DB date representation
965 # using this format that does not depend on DB date representation
966 time.strftime("%Y-%m-%d %H:%M:%S")
966 time.strftime("%Y-%m-%d %H:%M:%S")
967 else
967 else
968 self.class.connection.quoted_date(time)
968 self.class.connection.quoted_date(time)
969 end
969 end
970 end
970 end
971
971
972 def date_for_user_time_zone(y, m, d)
972 def date_for_user_time_zone(y, m, d)
973 if tz = User.current.time_zone
973 if tz = User.current.time_zone
974 tz.local y, m, d
974 tz.local y, m, d
975 else
975 else
976 Time.local y, m, d
976 Time.local y, m, d
977 end
977 end
978 end
978 end
979
979
980 # Returns a SQL clause for a date or datetime field.
980 # Returns a SQL clause for a date or datetime field.
981 def date_clause(table, field, from, to, is_custom_filter)
981 def date_clause(table, field, from, to, is_custom_filter)
982 s = []
982 s = []
983 if from
983 if from
984 if from.is_a?(Date)
984 if from.is_a?(Date)
985 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
985 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
986 else
986 else
987 from = from - 1 # second
987 from = from - 1 # second
988 end
988 end
989 if self.class.default_timezone == :utc
989 if self.class.default_timezone == :utc
990 from = from.utc
990 from = from.utc
991 end
991 end
992 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
992 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
993 end
993 end
994 if to
994 if to
995 if to.is_a?(Date)
995 if to.is_a?(Date)
996 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
996 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
997 end
997 end
998 if self.class.default_timezone == :utc
998 if self.class.default_timezone == :utc
999 to = to.utc
999 to = to.utc
1000 end
1000 end
1001 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1001 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1002 end
1002 end
1003 s.join(' AND ')
1003 s.join(' AND ')
1004 end
1004 end
1005
1005
1006 # Returns a SQL clause for a date or datetime field using relative dates.
1006 # Returns a SQL clause for a date or datetime field using relative dates.
1007 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1007 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1008 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1008 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1009 end
1009 end
1010
1010
1011 # Returns a Date or Time from the given filter value
1011 # Returns a Date or Time from the given filter value
1012 def parse_date(arg)
1012 def parse_date(arg)
1013 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1013 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1014 Time.parse(arg) rescue nil
1014 Time.parse(arg) rescue nil
1015 else
1015 else
1016 Date.parse(arg) rescue nil
1016 Date.parse(arg) rescue nil
1017 end
1017 end
1018 end
1018 end
1019
1019
1020 # Additional joins required for the given sort options
1020 # Additional joins required for the given sort options
1021 def joins_for_order_statement(order_options)
1021 def joins_for_order_statement(order_options)
1022 joins = []
1022 joins = []
1023
1023
1024 if order_options
1024 if order_options
1025 if order_options.include?('authors')
1025 if order_options.include?('authors')
1026 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1026 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1027 end
1027 end
1028 order_options.scan(/cf_\d+/).uniq.each do |name|
1028 order_options.scan(/cf_\d+/).uniq.each do |name|
1029 column = available_columns.detect {|c| c.name.to_s == name}
1029 column = available_columns.detect {|c| c.name.to_s == name}
1030 join = column && column.custom_field.join_for_order_statement
1030 join = column && column.custom_field.join_for_order_statement
1031 if join
1031 if join
1032 joins << join
1032 joins << join
1033 end
1033 end
1034 end
1034 end
1035 end
1035 end
1036
1036
1037 joins.any? ? joins.join(' ') : nil
1037 joins.any? ? joins.join(' ') : nil
1038 end
1038 end
1039 end
1039 end
@@ -1,114 +1,137
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Tracker < ActiveRecord::Base
18 class Tracker < ActiveRecord::Base
19
19
20 CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
20 CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
21 # Fields that can be disabled
21 # Fields that can be disabled
22 # Other (future) fields should be appended, not inserted!
22 # Other (future) fields should be appended, not inserted!
23 CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
23 CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
24 CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
24 CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
25
25
26 before_destroy :check_integrity
26 before_destroy :check_integrity
27 belongs_to :default_status, :class_name => 'IssueStatus'
27 belongs_to :default_status, :class_name => 'IssueStatus'
28 has_many :issues
28 has_many :issues
29 has_many :workflow_rules, :dependent => :delete_all do
29 has_many :workflow_rules, :dependent => :delete_all do
30 def copy(source_tracker)
30 def copy(source_tracker)
31 WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
31 WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
32 end
32 end
33 end
33 end
34
34
35 has_and_belongs_to_many :projects
35 has_and_belongs_to_many :projects
36 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
36 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
37 acts_as_positioned
37 acts_as_positioned
38
38
39 attr_protected :fields_bits
39 attr_protected :fields_bits
40
40
41 validates_presence_of :default_status
41 validates_presence_of :default_status
42 validates_presence_of :name
42 validates_presence_of :name
43 validates_uniqueness_of :name
43 validates_uniqueness_of :name
44 validates_length_of :name, :maximum => 30
44 validates_length_of :name, :maximum => 30
45
45
46 scope :sorted, lambda { order(:position) }
46 scope :sorted, lambda { order(:position) }
47 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
47 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
48
48
49 # Returns the trackers that are visible by the user.
50 #
51 # Examples:
52 # project.trackers.visible(user)
53 # => returns the trackers that are visible by the user in project
54 #
55 # Tracker.visible(user)
56 # => returns the trackers that are visible by the user in at least on project
57 scope :visible, lambda {|*args|
58 user = args.shift || User.current
59 condition = Project.allowed_to_condition(user, :view_issues) do |role, user|
60 unless role.permissions_all_trackers?(:view_issues)
61 tracker_ids = role.permissions_tracker_ids(:view_issues)
62 if tracker_ids.any?
63 "#{Tracker.table_name}.id IN (#{tracker_ids.join(',')})"
64 else
65 '1=0'
66 end
67 end
68 end
69 joins(:projects).where(condition).uniq
70 }
71
49 def to_s; name end
72 def to_s; name end
50
73
51 def <=>(tracker)
74 def <=>(tracker)
52 position <=> tracker.position
75 position <=> tracker.position
53 end
76 end
54
77
55 # Returns an array of IssueStatus that are used
78 # Returns an array of IssueStatus that are used
56 # in the tracker's workflows
79 # in the tracker's workflows
57 def issue_statuses
80 def issue_statuses
58 @issue_statuses ||= IssueStatus.where(:id => issue_status_ids).to_a.sort
81 @issue_statuses ||= IssueStatus.where(:id => issue_status_ids).to_a.sort
59 end
82 end
60
83
61 def issue_status_ids
84 def issue_status_ids
62 if new_record?
85 if new_record?
63 []
86 []
64 else
87 else
65 @issue_status_ids ||= WorkflowTransition.where(:tracker_id => id).uniq.pluck(:old_status_id, :new_status_id).flatten.uniq
88 @issue_status_ids ||= WorkflowTransition.where(:tracker_id => id).uniq.pluck(:old_status_id, :new_status_id).flatten.uniq
66 end
89 end
67 end
90 end
68
91
69 def disabled_core_fields
92 def disabled_core_fields
70 i = -1
93 i = -1
71 @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0}
94 @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0}
72 end
95 end
73
96
74 def core_fields
97 def core_fields
75 CORE_FIELDS - disabled_core_fields
98 CORE_FIELDS - disabled_core_fields
76 end
99 end
77
100
78 def core_fields=(fields)
101 def core_fields=(fields)
79 raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array)
102 raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array)
80
103
81 bits = 0
104 bits = 0
82 CORE_FIELDS.each_with_index do |field, i|
105 CORE_FIELDS.each_with_index do |field, i|
83 unless fields.include?(field)
106 unless fields.include?(field)
84 bits |= 2 ** i
107 bits |= 2 ** i
85 end
108 end
86 end
109 end
87 self.fields_bits = bits
110 self.fields_bits = bits
88 @disabled_core_fields = nil
111 @disabled_core_fields = nil
89 core_fields
112 core_fields
90 end
113 end
91
114
92 # Returns the fields that are disabled for all the given trackers
115 # Returns the fields that are disabled for all the given trackers
93 def self.disabled_core_fields(trackers)
116 def self.disabled_core_fields(trackers)
94 if trackers.present?
117 if trackers.present?
95 trackers.map(&:disabled_core_fields).reduce(:&)
118 trackers.map(&:disabled_core_fields).reduce(:&)
96 else
119 else
97 []
120 []
98 end
121 end
99 end
122 end
100
123
101 # Returns the fields that are enabled for one tracker at least
124 # Returns the fields that are enabled for one tracker at least
102 def self.core_fields(trackers)
125 def self.core_fields(trackers)
103 if trackers.present?
126 if trackers.present?
104 trackers.uniq.map(&:core_fields).reduce(:|)
127 trackers.uniq.map(&:core_fields).reduce(:|)
105 else
128 else
106 CORE_FIELDS.dup
129 CORE_FIELDS.dup
107 end
130 end
108 end
131 end
109
132
110 private
133 private
111 def check_integrity
134 def check_integrity
112 raise Exception.new("Cannot delete tracker") if Issue.where(:tracker_id => self.id).any?
135 raise Exception.new("Cannot delete tracker") if Issue.where(:tracker_id => self.id).any?
113 end
136 end
114 end
137 end
@@ -1,119 +1,131
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class TrackerTest < ActiveSupport::TestCase
20 class TrackerTest < ActiveSupport::TestCase
21 fixtures :trackers, :workflows, :issue_statuses, :roles, :issues
21 fixtures :trackers, :workflows, :issue_statuses, :roles, :issues, :projects, :projects_trackers
22
22
23 def test_sorted_scope
23 def test_sorted_scope
24 assert_equal Tracker.all.sort, Tracker.sorted.to_a
24 assert_equal Tracker.all.sort, Tracker.sorted.to_a
25 end
25 end
26
26
27 def test_named_scope
27 def test_named_scope
28 assert_equal Tracker.find_by_name('Feature'), Tracker.named('feature').first
28 assert_equal Tracker.find_by_name('Feature'), Tracker.named('feature').first
29 end
29 end
30
30
31 def test_visible_scope_chained_with_project_rolled_up_trackers
32 project = Project.find(1)
33 role = Role.generate!
34 role.add_permission! :view_issues
35 role.set_permission_trackers :view_issues, [2]
36 role.save!
37 user = User.generate!
38 User.add_to_project user, project, role
39
40 assert_equal [2], project.rolled_up_trackers(false).visible(user).map(&:id)
41 end
42
31 def test_copy_workflows
43 def test_copy_workflows
32 source = Tracker.find(1)
44 source = Tracker.find(1)
33 rules_count = source.workflow_rules.count
45 rules_count = source.workflow_rules.count
34 assert rules_count > 0
46 assert rules_count > 0
35
47
36 target = Tracker.new(:name => 'Target', :default_status_id => 1)
48 target = Tracker.new(:name => 'Target', :default_status_id => 1)
37 assert target.save
49 assert target.save
38 target.workflow_rules.copy(source)
50 target.workflow_rules.copy(source)
39 target.reload
51 target.reload
40 assert_equal rules_count, target.workflow_rules.size
52 assert_equal rules_count, target.workflow_rules.size
41 end
53 end
42
54
43 def test_issue_statuses
55 def test_issue_statuses
44 tracker = Tracker.find(1)
56 tracker = Tracker.find(1)
45 WorkflowTransition.delete_all
57 WorkflowTransition.delete_all
46 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
58 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
47 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
59 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
48
60
49 assert_kind_of Array, tracker.issue_statuses
61 assert_kind_of Array, tracker.issue_statuses
50 assert_kind_of IssueStatus, tracker.issue_statuses.first
62 assert_kind_of IssueStatus, tracker.issue_statuses.first
51 assert_equal [2, 3, 5], Tracker.find(1).issue_statuses.collect(&:id)
63 assert_equal [2, 3, 5], Tracker.find(1).issue_statuses.collect(&:id)
52 end
64 end
53
65
54 def test_issue_statuses_empty
66 def test_issue_statuses_empty
55 WorkflowTransition.delete_all("tracker_id = 1")
67 WorkflowTransition.delete_all("tracker_id = 1")
56 assert_equal [], Tracker.find(1).issue_statuses
68 assert_equal [], Tracker.find(1).issue_statuses
57 end
69 end
58
70
59 def test_issue_statuses_should_be_empty_for_new_record
71 def test_issue_statuses_should_be_empty_for_new_record
60 assert_equal [], Tracker.new.issue_statuses
72 assert_equal [], Tracker.new.issue_statuses
61 end
73 end
62
74
63 def test_core_fields_should_be_enabled_by_default
75 def test_core_fields_should_be_enabled_by_default
64 tracker = Tracker.new
76 tracker = Tracker.new
65 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
77 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
66 assert_equal [], tracker.disabled_core_fields
78 assert_equal [], tracker.disabled_core_fields
67 end
79 end
68
80
69 def test_core_fields
81 def test_core_fields
70 tracker = Tracker.new
82 tracker = Tracker.new
71 tracker.core_fields = %w(assigned_to_id due_date)
83 tracker.core_fields = %w(assigned_to_id due_date)
72
84
73 assert_equal %w(assigned_to_id due_date), tracker.core_fields
85 assert_equal %w(assigned_to_id due_date), tracker.core_fields
74 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date), tracker.disabled_core_fields
86 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date), tracker.disabled_core_fields
75 end
87 end
76
88
77 def test_core_fields_should_return_fields_enabled_for_any_tracker
89 def test_core_fields_should_return_fields_enabled_for_any_tracker
78 trackers = []
90 trackers = []
79 trackers << Tracker.new(:core_fields => %w(assigned_to_id due_date))
91 trackers << Tracker.new(:core_fields => %w(assigned_to_id due_date))
80 trackers << Tracker.new(:core_fields => %w(assigned_to_id done_ratio))
92 trackers << Tracker.new(:core_fields => %w(assigned_to_id done_ratio))
81 trackers << Tracker.new(:core_fields => [])
93 trackers << Tracker.new(:core_fields => [])
82
94
83 assert_equal %w(assigned_to_id due_date done_ratio), Tracker.core_fields(trackers)
95 assert_equal %w(assigned_to_id due_date done_ratio), Tracker.core_fields(trackers)
84 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date done_ratio), Tracker.disabled_core_fields(trackers)
96 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date done_ratio), Tracker.disabled_core_fields(trackers)
85 end
97 end
86
98
87 def test_core_fields_should_return_all_fields_for_an_empty_argument
99 def test_core_fields_should_return_all_fields_for_an_empty_argument
88 assert_equal Tracker::CORE_FIELDS, Tracker.core_fields([])
100 assert_equal Tracker::CORE_FIELDS, Tracker.core_fields([])
89 assert_equal [], Tracker.disabled_core_fields([])
101 assert_equal [], Tracker.disabled_core_fields([])
90 end
102 end
91
103
92 def test_sort_should_sort_by_position
104 def test_sort_should_sort_by_position
93 a = Tracker.new(:name => 'Tracker A', :position => 2)
105 a = Tracker.new(:name => 'Tracker A', :position => 2)
94 b = Tracker.new(:name => 'Tracker B', :position => 1)
106 b = Tracker.new(:name => 'Tracker B', :position => 1)
95
107
96 assert_equal [b, a], [a, b].sort
108 assert_equal [b, a], [a, b].sort
97 end
109 end
98
110
99 def test_destroying_a_tracker_without_issues_should_not_raise_an_error
111 def test_destroying_a_tracker_without_issues_should_not_raise_an_error
100 tracker = Tracker.find(1)
112 tracker = Tracker.find(1)
101 Issue.delete_all :tracker_id => tracker.id
113 Issue.delete_all :tracker_id => tracker.id
102
114
103 assert_difference 'Tracker.count', -1 do
115 assert_difference 'Tracker.count', -1 do
104 assert_nothing_raised do
116 assert_nothing_raised do
105 tracker.destroy
117 tracker.destroy
106 end
118 end
107 end
119 end
108 end
120 end
109
121
110 def test_destroying_a_tracker_with_issues_should_raise_an_error
122 def test_destroying_a_tracker_with_issues_should_raise_an_error
111 tracker = Tracker.find(1)
123 tracker = Tracker.find(1)
112
124
113 assert_no_difference 'Tracker.count' do
125 assert_no_difference 'Tracker.count' do
114 assert_raise Exception do
126 assert_raise Exception do
115 tracker.destroy
127 tracker.destroy
116 end
128 end
117 end
129 end
118 end
130 end
119 end
131 end
General Comments 0
You need to be logged in to leave comments. Login now