##// END OF EJS Templates
Adds pagination to admin project list....
Jean-Philippe Lang -
r15373:daab32923d85
parent child
Show More
@@ -1,84 +1,87
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 AdminController < ApplicationController
18 class AdminController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20 menu_item :projects, :only => :projects
20 menu_item :projects, :only => :projects
21 menu_item :plugins, :only => :plugins
21 menu_item :plugins, :only => :plugins
22 menu_item :info, :only => :info
22 menu_item :info, :only => :info
23
23
24 before_action :require_admin
24 before_action :require_admin
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27
27
28 def index
28 def index
29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 end
30 end
31
31
32 def projects
32 def projects
33 @status = params[:status] || 1
33 @status = params[:status] || 1
34
34
35 scope = Project.status(@status).sorted
35 scope = Project.status(@status).sorted
36 scope = scope.like(params[:name]) if params[:name].present?
36 scope = scope.like(params[:name]) if params[:name].present?
37 @projects = scope.to_a
37
38 @project_count = scope.count
39 @project_pages = Paginator.new @project_count, per_page_option, params['page']
40 @projects = scope.limit(@project_pages.per_page).offset(@project_pages.offset).to_a
38
41
39 render :action => "projects", :layout => false if request.xhr?
42 render :action => "projects", :layout => false if request.xhr?
40 end
43 end
41
44
42 def plugins
45 def plugins
43 @plugins = Redmine::Plugin.all
46 @plugins = Redmine::Plugin.all
44 end
47 end
45
48
46 # Loads the default configuration
49 # Loads the default configuration
47 # (roles, trackers, statuses, workflow, enumerations)
50 # (roles, trackers, statuses, workflow, enumerations)
48 def default_configuration
51 def default_configuration
49 if request.post?
52 if request.post?
50 begin
53 begin
51 Redmine::DefaultData::Loader::load(params[:lang])
54 Redmine::DefaultData::Loader::load(params[:lang])
52 flash[:notice] = l(:notice_default_data_loaded)
55 flash[:notice] = l(:notice_default_data_loaded)
53 rescue Exception => e
56 rescue Exception => e
54 flash[:error] = l(:error_can_t_load_default_data, ERB::Util.h(e.message))
57 flash[:error] = l(:error_can_t_load_default_data, ERB::Util.h(e.message))
55 end
58 end
56 end
59 end
57 redirect_to admin_path
60 redirect_to admin_path
58 end
61 end
59
62
60 def test_email
63 def test_email
61 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
64 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
62 # Force ActionMailer to raise delivery errors so we can catch it
65 # Force ActionMailer to raise delivery errors so we can catch it
63 ActionMailer::Base.raise_delivery_errors = true
66 ActionMailer::Base.raise_delivery_errors = true
64 begin
67 begin
65 @test = Mailer.test_email(User.current).deliver
68 @test = Mailer.test_email(User.current).deliver
66 flash[:notice] = l(:notice_email_sent, ERB::Util.h(User.current.mail))
69 flash[:notice] = l(:notice_email_sent, ERB::Util.h(User.current.mail))
67 rescue Exception => e
70 rescue Exception => e
68 flash[:error] = l(:notice_email_error, ERB::Util.h(Redmine::CodesetUtil.replace_invalid_utf8(e.message.dup)))
71 flash[:error] = l(:notice_email_error, ERB::Util.h(Redmine::CodesetUtil.replace_invalid_utf8(e.message.dup)))
69 end
72 end
70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
73 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
71 redirect_to settings_path(:tab => 'notifications')
74 redirect_to settings_path(:tab => 'notifications')
72 end
75 end
73
76
74 def info
77 def info
75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
78 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
76 @checklist = [
79 @checklist = [
77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
80 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
81 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
79 ["#{l :text_plugin_assets_writable} (./public/plugin_assets)", File.writable?(Redmine::Plugin.public_directory)],
82 ["#{l :text_plugin_assets_writable} (./public/plugin_assets)", File.writable?(Redmine::Plugin.public_directory)],
80 [:text_rmagick_available, Object.const_defined?(:Magick)],
83 [:text_rmagick_available, Object.const_defined?(:Magick)],
81 [:text_convert_available, Redmine::Thumbnail.convert_available?]
84 [:text_convert_available, Redmine::Thumbnail.convert_available?]
82 ]
85 ]
83 end
86 end
84 end
87 end
@@ -1,228 +1,228
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_action :find_project, :except => [ :index, :list, :new, :create, :copy ]
22 before_action :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_action :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
23 before_action :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_action :authorize_global, :only => [:new, :create]
24 before_action :authorize_global, :only => [:new, :create]
25 before_action :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
25 before_action :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 helper :custom_fields
30 helper :custom_fields
31 helper :issues
31 helper :issues
32 helper :queries
32 helper :queries
33 helper :repositories
33 helper :repositories
34 helper :members
34 helper :members
35
35
36 # Lists visible projects
36 # Lists visible projects
37 def index
37 def index
38 scope = Project.visible.sorted
38 scope = Project.visible.sorted
39
39
40 respond_to do |format|
40 respond_to do |format|
41 format.html {
41 format.html {
42 unless params[:closed]
42 unless params[:closed]
43 scope = scope.active
43 scope = scope.active
44 end
44 end
45 @projects = scope.to_a
45 @projects = scope.to_a
46 }
46 }
47 format.api {
47 format.api {
48 @offset, @limit = api_offset_and_limit
48 @offset, @limit = api_offset_and_limit
49 @project_count = scope.count
49 @project_count = scope.count
50 @projects = scope.offset(@offset).limit(@limit).to_a
50 @projects = scope.offset(@offset).limit(@limit).to_a
51 }
51 }
52 format.atom {
52 format.atom {
53 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
53 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
54 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
54 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
55 }
55 }
56 end
56 end
57 end
57 end
58
58
59 def new
59 def new
60 @issue_custom_fields = IssueCustomField.sorted.to_a
60 @issue_custom_fields = IssueCustomField.sorted.to_a
61 @trackers = Tracker.sorted.to_a
61 @trackers = Tracker.sorted.to_a
62 @project = Project.new
62 @project = Project.new
63 @project.safe_attributes = params[:project]
63 @project.safe_attributes = params[:project]
64 end
64 end
65
65
66 def create
66 def create
67 @issue_custom_fields = IssueCustomField.sorted.to_a
67 @issue_custom_fields = IssueCustomField.sorted.to_a
68 @trackers = Tracker.sorted.to_a
68 @trackers = Tracker.sorted.to_a
69 @project = Project.new
69 @project = Project.new
70 @project.safe_attributes = params[:project]
70 @project.safe_attributes = params[:project]
71
71
72 if @project.save
72 if @project.save
73 unless User.current.admin?
73 unless User.current.admin?
74 @project.add_default_member(User.current)
74 @project.add_default_member(User.current)
75 end
75 end
76 respond_to do |format|
76 respond_to do |format|
77 format.html {
77 format.html {
78 flash[:notice] = l(:notice_successful_create)
78 flash[:notice] = l(:notice_successful_create)
79 if params[:continue]
79 if params[:continue]
80 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
80 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
81 redirect_to new_project_path(attrs)
81 redirect_to new_project_path(attrs)
82 else
82 else
83 redirect_to settings_project_path(@project)
83 redirect_to settings_project_path(@project)
84 end
84 end
85 }
85 }
86 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
86 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
87 end
87 end
88 else
88 else
89 respond_to do |format|
89 respond_to do |format|
90 format.html { render :action => 'new' }
90 format.html { render :action => 'new' }
91 format.api { render_validation_errors(@project) }
91 format.api { render_validation_errors(@project) }
92 end
92 end
93 end
93 end
94 end
94 end
95
95
96 def copy
96 def copy
97 @issue_custom_fields = IssueCustomField.sorted.to_a
97 @issue_custom_fields = IssueCustomField.sorted.to_a
98 @trackers = Tracker.sorted.to_a
98 @trackers = Tracker.sorted.to_a
99 @source_project = Project.find(params[:id])
99 @source_project = Project.find(params[:id])
100 if request.get?
100 if request.get?
101 @project = Project.copy_from(@source_project)
101 @project = Project.copy_from(@source_project)
102 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
102 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
103 else
103 else
104 Mailer.with_deliveries(params[:notifications] == '1') do
104 Mailer.with_deliveries(params[:notifications] == '1') do
105 @project = Project.new
105 @project = Project.new
106 @project.safe_attributes = params[:project]
106 @project.safe_attributes = params[:project]
107 if @project.copy(@source_project, :only => params[:only])
107 if @project.copy(@source_project, :only => params[:only])
108 flash[:notice] = l(:notice_successful_create)
108 flash[:notice] = l(:notice_successful_create)
109 redirect_to settings_project_path(@project)
109 redirect_to settings_project_path(@project)
110 elsif !@project.new_record?
110 elsif !@project.new_record?
111 # Project was created
111 # Project was created
112 # But some objects were not copied due to validation failures
112 # But some objects were not copied due to validation failures
113 # (eg. issues from disabled trackers)
113 # (eg. issues from disabled trackers)
114 # TODO: inform about that
114 # TODO: inform about that
115 redirect_to settings_project_path(@project)
115 redirect_to settings_project_path(@project)
116 end
116 end
117 end
117 end
118 end
118 end
119 rescue ActiveRecord::RecordNotFound
119 rescue ActiveRecord::RecordNotFound
120 # source_project not found
120 # source_project not found
121 render_404
121 render_404
122 end
122 end
123
123
124 # Show @project
124 # Show @project
125 def show
125 def show
126 # try to redirect to the requested menu item
126 # try to redirect to the requested menu item
127 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
127 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
128 return
128 return
129 end
129 end
130
130
131 @users_by_role = @project.users_by_role
131 @users_by_role = @project.users_by_role
132 @subprojects = @project.children.visible.to_a
132 @subprojects = @project.children.visible.to_a
133 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
133 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
134 @trackers = @project.rolled_up_trackers.visible
134 @trackers = @project.rolled_up_trackers.visible
135
135
136 cond = @project.project_condition(Setting.display_subprojects_issues?)
136 cond = @project.project_condition(Setting.display_subprojects_issues?)
137
137
138 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
138 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
139 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
139 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
140
140
141 if User.current.allowed_to_view_all_time_entries?(@project)
141 if User.current.allowed_to_view_all_time_entries?(@project)
142 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
142 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
143 end
143 end
144
144
145 @key = User.current.rss_key
145 @key = User.current.rss_key
146
146
147 respond_to do |format|
147 respond_to do |format|
148 format.html
148 format.html
149 format.api
149 format.api
150 end
150 end
151 end
151 end
152
152
153 def settings
153 def settings
154 @issue_custom_fields = IssueCustomField.sorted.to_a
154 @issue_custom_fields = IssueCustomField.sorted.to_a
155 @issue_category ||= IssueCategory.new
155 @issue_category ||= IssueCategory.new
156 @member ||= @project.members.new
156 @member ||= @project.members.new
157 @trackers = Tracker.sorted.to_a
157 @trackers = Tracker.sorted.to_a
158 @wiki ||= @project.wiki || Wiki.new(:project => @project)
158 @wiki ||= @project.wiki || Wiki.new(:project => @project)
159 end
159 end
160
160
161 def edit
161 def edit
162 end
162 end
163
163
164 def update
164 def update
165 @project.safe_attributes = params[:project]
165 @project.safe_attributes = params[:project]
166 if @project.save
166 if @project.save
167 respond_to do |format|
167 respond_to do |format|
168 format.html {
168 format.html {
169 flash[:notice] = l(:notice_successful_update)
169 flash[:notice] = l(:notice_successful_update)
170 redirect_to settings_project_path(@project)
170 redirect_to settings_project_path(@project)
171 }
171 }
172 format.api { render_api_ok }
172 format.api { render_api_ok }
173 end
173 end
174 else
174 else
175 respond_to do |format|
175 respond_to do |format|
176 format.html {
176 format.html {
177 settings
177 settings
178 render :action => 'settings'
178 render :action => 'settings'
179 }
179 }
180 format.api { render_validation_errors(@project) }
180 format.api { render_validation_errors(@project) }
181 end
181 end
182 end
182 end
183 end
183 end
184
184
185 def modules
185 def modules
186 @project.enabled_module_names = params[:enabled_module_names]
186 @project.enabled_module_names = params[:enabled_module_names]
187 flash[:notice] = l(:notice_successful_update)
187 flash[:notice] = l(:notice_successful_update)
188 redirect_to settings_project_path(@project, :tab => 'modules')
188 redirect_to settings_project_path(@project, :tab => 'modules')
189 end
189 end
190
190
191 def archive
191 def archive
192 unless @project.archive
192 unless @project.archive
193 flash[:error] = l(:error_can_not_archive_project)
193 flash[:error] = l(:error_can_not_archive_project)
194 end
194 end
195 redirect_to admin_projects_path(:status => params[:status])
195 redirect_to_referer_or admin_projects_path(:status => params[:status])
196 end
196 end
197
197
198 def unarchive
198 def unarchive
199 unless @project.active?
199 unless @project.active?
200 @project.unarchive
200 @project.unarchive
201 end
201 end
202 redirect_to admin_projects_path(:status => params[:status])
202 redirect_to_referer_or admin_projects_path(:status => params[:status])
203 end
203 end
204
204
205 def close
205 def close
206 @project.close
206 @project.close
207 redirect_to project_path(@project)
207 redirect_to project_path(@project)
208 end
208 end
209
209
210 def reopen
210 def reopen
211 @project.reopen
211 @project.reopen
212 redirect_to project_path(@project)
212 redirect_to project_path(@project)
213 end
213 end
214
214
215 # Delete @project
215 # Delete @project
216 def destroy
216 def destroy
217 @project_to_destroy = @project
217 @project_to_destroy = @project
218 if api_request? || params[:confirm]
218 if api_request? || params[:confirm]
219 @project_to_destroy.destroy
219 @project_to_destroy.destroy
220 respond_to do |format|
220 respond_to do |format|
221 format.html { redirect_to admin_projects_path }
221 format.html { redirect_to admin_projects_path }
222 format.api { render_api_ok }
222 format.api { render_api_ok }
223 end
223 end
224 end
224 end
225 # hide project in layout
225 # hide project in layout
226 @project = nil
226 @project = nil
227 end
227 end
228 end
228 end
@@ -1,1371 +1,1371
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31 include Redmine::Helpers::URL
31 include Redmine::Helpers::URL
32
32
33 extend Forwardable
33 extend Forwardable
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35
35
36 # Return true if user is authorized for controller/action, otherwise false
36 # Return true if user is authorized for controller/action, otherwise false
37 def authorize_for(controller, action)
37 def authorize_for(controller, action)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 end
39 end
40
40
41 # Display a link if user is authorized
41 # Display a link if user is authorized
42 #
42 #
43 # @param [String] name Anchor text (passed to link_to)
43 # @param [String] name Anchor text (passed to link_to)
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] html_options Options passed to link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 end
49 end
50
50
51 # Displays a link to user's account page if active
51 # Displays a link to user's account page if active
52 def link_to_user(user, options={})
52 def link_to_user(user, options={})
53 if user.is_a?(User)
53 if user.is_a?(User)
54 name = h(user.name(options[:format]))
54 name = h(user.name(options[:format]))
55 if user.active? || (User.current.admin? && user.logged?)
55 if user.active? || (User.current.admin? && user.logged?)
56 link_to name, user_path(user), :class => user.css_classes
56 link_to name, user_path(user), :class => user.css_classes
57 else
57 else
58 name
58 name
59 end
59 end
60 else
60 else
61 h(user.to_s)
61 h(user.to_s)
62 end
62 end
63 end
63 end
64
64
65 # Displays a link to +issue+ with its subject.
65 # Displays a link to +issue+ with its subject.
66 # Examples:
66 # Examples:
67 #
67 #
68 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 if options[:subject] == false
78 if options[:subject] == false
79 title = issue.subject.truncate(60)
79 title = issue.subject.truncate(60)
80 else
80 else
81 subject = issue.subject
81 subject = issue.subject
82 if truncate_length = options[:truncate]
82 if truncate_length = options[:truncate]
83 subject = subject.truncate(truncate_length)
83 subject = subject.truncate(truncate_length)
84 end
84 end
85 end
85 end
86 only_path = options[:only_path].nil? ? true : options[:only_path]
86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 s = link_to(text, issue_url(issue, :only_path => only_path),
87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 :class => issue.css_classes, :title => title)
88 :class => issue.css_classes, :title => title)
89 s << h(": #{subject}") if subject
89 s << h(": #{subject}") if subject
90 s = h("#{issue.project} - ") + s if options[:project]
90 s = h("#{issue.project} - ") + s if options[:project]
91 s
91 s
92 end
92 end
93
93
94 # Generates a link to an attachment.
94 # Generates a link to an attachment.
95 # Options:
95 # Options:
96 # * :text - Link text (default to attachment filename)
96 # * :text - Link text (default to attachment filename)
97 # * :download - Force download (default: false)
97 # * :download - Force download (default: false)
98 def link_to_attachment(attachment, options={})
98 def link_to_attachment(attachment, options={})
99 text = options.delete(:text) || attachment.filename
99 text = options.delete(:text) || attachment.filename
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 html_options = options.slice!(:only_path)
101 html_options = options.slice!(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
103 url = send(route_method, attachment, attachment.filename, options)
103 url = send(route_method, attachment, attachment.filename, options)
104 link_to text, url, html_options
104 link_to text, url, html_options
105 end
105 end
106
106
107 # Generates a link to a SCM revision
107 # Generates a link to a SCM revision
108 # Options:
108 # Options:
109 # * :text - Link text (default to the formatted revision)
109 # * :text - Link text (default to the formatted revision)
110 def link_to_revision(revision, repository, options={})
110 def link_to_revision(revision, repository, options={})
111 if repository.is_a?(Project)
111 if repository.is_a?(Project)
112 repository = repository.repository
112 repository = repository.repository
113 end
113 end
114 text = options.delete(:text) || format_revision(revision)
114 text = options.delete(:text) || format_revision(revision)
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 link_to(
116 link_to(
117 h(text),
117 h(text),
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 :title => l(:label_revision_id, format_revision(revision)),
119 :title => l(:label_revision_id, format_revision(revision)),
120 :accesskey => options[:accesskey]
120 :accesskey => options[:accesskey]
121 )
121 )
122 end
122 end
123
123
124 # Generates a link to a message
124 # Generates a link to a message
125 def link_to_message(message, options={}, html_options = nil)
125 def link_to_message(message, options={}, html_options = nil)
126 link_to(
126 link_to(
127 message.subject.truncate(60),
127 message.subject.truncate(60),
128 board_message_url(message.board_id, message.parent_id || message.id, {
128 board_message_url(message.board_id, message.parent_id || message.id, {
129 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 :only_path => true
131 :only_path => true
132 }.merge(options)),
132 }.merge(options)),
133 html_options
133 html_options
134 )
134 )
135 end
135 end
136
136
137 # Generates a link to a project if active
137 # Generates a link to a project if active
138 # Examples:
138 # Examples:
139 #
139 #
140 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project) # => link to the specified project overview
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 #
143 #
144 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
145 if project.archived?
145 if project.archived?
146 h(project.name)
146 h(project.name)
147 else
147 else
148 link_to project.name,
148 link_to project.name,
149 project_url(project, {:only_path => true}.merge(options)),
149 project_url(project, {:only_path => true}.merge(options)),
150 html_options
150 html_options
151 end
151 end
152 end
152 end
153
153
154 # Generates a link to a project settings if active
154 # Generates a link to a project settings if active
155 def link_to_project_settings(project, options={}, html_options=nil)
155 def link_to_project_settings(project, options={}, html_options=nil)
156 if project.active?
156 if project.active?
157 link_to project.name, settings_project_path(project, options), html_options
157 link_to project.name, settings_project_path(project, options), html_options
158 elsif project.archived?
158 elsif project.archived?
159 h(project.name)
159 h(project.name)
160 else
160 else
161 link_to project.name, project_path(project, options), html_options
161 link_to project.name, project_path(project, options), html_options
162 end
162 end
163 end
163 end
164
164
165 # Generates a link to a version
165 # Generates a link to a version
166 def link_to_version(version, options = {})
166 def link_to_version(version, options = {})
167 return '' unless version && version.is_a?(Version)
167 return '' unless version && version.is_a?(Version)
168 options = {:title => format_date(version.effective_date)}.merge(options)
168 options = {:title => format_date(version.effective_date)}.merge(options)
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 end
170 end
171
171
172 # Helper that formats object for html or text rendering
172 # Helper that formats object for html or text rendering
173 def format_object(object, html=true, &block)
173 def format_object(object, html=true, &block)
174 if block_given?
174 if block_given?
175 object = yield object
175 object = yield object
176 end
176 end
177 case object.class.name
177 case object.class.name
178 when 'Array'
178 when 'Array'
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 when 'Time'
180 when 'Time'
181 format_time(object)
181 format_time(object)
182 when 'Date'
182 when 'Date'
183 format_date(object)
183 format_date(object)
184 when 'Fixnum'
184 when 'Fixnum'
185 object.to_s
185 object.to_s
186 when 'Float'
186 when 'Float'
187 sprintf "%.2f", object
187 sprintf "%.2f", object
188 when 'User'
188 when 'User'
189 html ? link_to_user(object) : object.to_s
189 html ? link_to_user(object) : object.to_s
190 when 'Project'
190 when 'Project'
191 html ? link_to_project(object) : object.to_s
191 html ? link_to_project(object) : object.to_s
192 when 'Version'
192 when 'Version'
193 html ? link_to_version(object) : object.to_s
193 html ? link_to_version(object) : object.to_s
194 when 'TrueClass'
194 when 'TrueClass'
195 l(:general_text_Yes)
195 l(:general_text_Yes)
196 when 'FalseClass'
196 when 'FalseClass'
197 l(:general_text_No)
197 l(:general_text_No)
198 when 'Issue'
198 when 'Issue'
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 when 'CustomValue', 'CustomFieldValue'
200 when 'CustomValue', 'CustomFieldValue'
201 if object.custom_field
201 if object.custom_field
202 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 f = object.custom_field.format.formatted_custom_value(self, object, html)
203 if f.nil? || f.is_a?(String)
203 if f.nil? || f.is_a?(String)
204 f
204 f
205 else
205 else
206 format_object(f, html, &block)
206 format_object(f, html, &block)
207 end
207 end
208 else
208 else
209 object.value.to_s
209 object.value.to_s
210 end
210 end
211 else
211 else
212 html ? h(object) : object.to_s
212 html ? h(object) : object.to_s
213 end
213 end
214 end
214 end
215
215
216 def wiki_page_path(page, options={})
216 def wiki_page_path(page, options={})
217 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
218 end
218 end
219
219
220 def thumbnail_tag(attachment)
220 def thumbnail_tag(attachment)
221 link_to image_tag(thumbnail_path(attachment)),
221 link_to image_tag(thumbnail_path(attachment)),
222 named_attachment_path(attachment, attachment.filename),
222 named_attachment_path(attachment, attachment.filename),
223 :title => attachment.filename
223 :title => attachment.filename
224 end
224 end
225
225
226 def toggle_link(name, id, options={})
226 def toggle_link(name, id, options={})
227 onclick = "$('##{id}').toggle(); "
227 onclick = "$('##{id}').toggle(); "
228 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
229 onclick << "return false;"
229 onclick << "return false;"
230 link_to(name, "#", :onclick => onclick)
230 link_to(name, "#", :onclick => onclick)
231 end
231 end
232
232
233 def format_activity_title(text)
233 def format_activity_title(text)
234 h(truncate_single_line_raw(text, 100))
234 h(truncate_single_line_raw(text, 100))
235 end
235 end
236
236
237 def format_activity_day(date)
237 def format_activity_day(date)
238 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 date == User.current.today ? l(:label_today).titleize : format_date(date)
239 end
239 end
240
240
241 def format_activity_description(text)
241 def format_activity_description(text)
242 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
243 ).gsub(/[\r\n]+/, "<br />").html_safe
243 ).gsub(/[\r\n]+/, "<br />").html_safe
244 end
244 end
245
245
246 def format_version_name(version)
246 def format_version_name(version)
247 if version.project == @project
247 if version.project == @project
248 h(version)
248 h(version)
249 else
249 else
250 h("#{version.project} - #{version}")
250 h("#{version.project} - #{version}")
251 end
251 end
252 end
252 end
253
253
254 def due_date_distance_in_words(date)
254 def due_date_distance_in_words(date)
255 if date
255 if date
256 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
256 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
257 end
257 end
258 end
258 end
259
259
260 # Renders a tree of projects as a nested set of unordered lists
260 # Renders a tree of projects as a nested set of unordered lists
261 # The given collection may be a subset of the whole project tree
261 # The given collection may be a subset of the whole project tree
262 # (eg. some intermediate nodes are private and can not be seen)
262 # (eg. some intermediate nodes are private and can not be seen)
263 def render_project_nested_lists(projects, &block)
263 def render_project_nested_lists(projects, &block)
264 s = ''
264 s = ''
265 if projects.any?
265 if projects.any?
266 ancestors = []
266 ancestors = []
267 original_project = @project
267 original_project = @project
268 projects.sort_by(&:lft).each do |project|
268 projects.sort_by(&:lft).each do |project|
269 # set the project environment to please macros.
269 # set the project environment to please macros.
270 @project = project
270 @project = project
271 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
272 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
273 else
273 else
274 ancestors.pop
274 ancestors.pop
275 s << "</li>"
275 s << "</li>"
276 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
277 ancestors.pop
277 ancestors.pop
278 s << "</ul></li>\n"
278 s << "</ul></li>\n"
279 end
279 end
280 end
280 end
281 classes = (ancestors.empty? ? 'root' : 'child')
281 classes = (ancestors.empty? ? 'root' : 'child')
282 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << "<li class='#{classes}'><div class='#{classes}'>"
283 s << h(block_given? ? capture(project, &block) : project.name)
283 s << h(block_given? ? capture(project, &block) : project.name)
284 s << "</div>\n"
284 s << "</div>\n"
285 ancestors << project
285 ancestors << project
286 end
286 end
287 s << ("</li></ul>\n" * ancestors.size)
287 s << ("</li></ul>\n" * ancestors.size)
288 @project = original_project
288 @project = original_project
289 end
289 end
290 s.html_safe
290 s.html_safe
291 end
291 end
292
292
293 def render_page_hierarchy(pages, node=nil, options={})
293 def render_page_hierarchy(pages, node=nil, options={})
294 content = ''
294 content = ''
295 if pages[node]
295 if pages[node]
296 content << "<ul class=\"pages-hierarchy\">\n"
296 content << "<ul class=\"pages-hierarchy\">\n"
297 pages[node].each do |page|
297 pages[node].each do |page|
298 content << "<li>"
298 content << "<li>"
299 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
300 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
301 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
302 content << "</li>\n"
302 content << "</li>\n"
303 end
303 end
304 content << "</ul>\n"
304 content << "</ul>\n"
305 end
305 end
306 content.html_safe
306 content.html_safe
307 end
307 end
308
308
309 # Renders flash messages
309 # Renders flash messages
310 def render_flash_messages
310 def render_flash_messages
311 s = ''
311 s = ''
312 flash.each do |k,v|
312 flash.each do |k,v|
313 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
314 end
314 end
315 s.html_safe
315 s.html_safe
316 end
316 end
317
317
318 # Renders tabs and their content
318 # Renders tabs and their content
319 def render_tabs(tabs, selected=params[:tab])
319 def render_tabs(tabs, selected=params[:tab])
320 if tabs.any?
320 if tabs.any?
321 unless tabs.detect {|tab| tab[:name] == selected}
321 unless tabs.detect {|tab| tab[:name] == selected}
322 selected = nil
322 selected = nil
323 end
323 end
324 selected ||= tabs.first[:name]
324 selected ||= tabs.first[:name]
325 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
326 else
326 else
327 content_tag 'p', l(:label_no_data), :class => "nodata"
327 content_tag 'p', l(:label_no_data), :class => "nodata"
328 end
328 end
329 end
329 end
330
330
331 # Renders the project quick-jump box
331 # Renders the project quick-jump box
332 def render_project_jump_box
332 def render_project_jump_box
333 return unless User.current.logged?
333 return unless User.current.logged?
334 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
335 if projects.any?
335 if projects.any?
336 options =
336 options =
337 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
338 '<option value="" disabled="disabled">---</option>').html_safe
338 '<option value="" disabled="disabled">---</option>').html_safe
339
339
340 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 options << project_tree_options_for_select(projects, :selected => @project) do |p|
341 { :value => project_path(:id => p, :jump => current_menu_item) }
341 { :value => project_path(:id => p, :jump => current_menu_item) }
342 end
342 end
343
343
344 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 content_tag( :span, nil, :class => 'jump-box-arrow') +
345 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
346 end
346 end
347 end
347 end
348
348
349 def project_tree_options_for_select(projects, options = {})
349 def project_tree_options_for_select(projects, options = {})
350 s = ''.html_safe
350 s = ''.html_safe
351 if blank_text = options[:include_blank]
351 if blank_text = options[:include_blank]
352 if blank_text == true
352 if blank_text == true
353 blank_text = '&nbsp;'.html_safe
353 blank_text = '&nbsp;'.html_safe
354 end
354 end
355 s << content_tag('option', blank_text, :value => '')
355 s << content_tag('option', blank_text, :value => '')
356 end
356 end
357 project_tree(projects) do |project, level|
357 project_tree(projects) do |project, level|
358 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
359 tag_options = {:value => project.id}
359 tag_options = {:value => project.id}
360 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
361 tag_options[:selected] = 'selected'
361 tag_options[:selected] = 'selected'
362 else
362 else
363 tag_options[:selected] = nil
363 tag_options[:selected] = nil
364 end
364 end
365 tag_options.merge!(yield(project)) if block_given?
365 tag_options.merge!(yield(project)) if block_given?
366 s << content_tag('option', name_prefix + h(project), tag_options)
366 s << content_tag('option', name_prefix + h(project), tag_options)
367 end
367 end
368 s.html_safe
368 s.html_safe
369 end
369 end
370
370
371 # Yields the given block for each project with its level in the tree
371 # Yields the given block for each project with its level in the tree
372 #
372 #
373 # Wrapper for Project#project_tree
373 # Wrapper for Project#project_tree
374 def project_tree(projects, &block)
374 def project_tree(projects, options={}, &block)
375 Project.project_tree(projects, &block)
375 Project.project_tree(projects, options, &block)
376 end
376 end
377
377
378 def principals_check_box_tags(name, principals)
378 def principals_check_box_tags(name, principals)
379 s = ''
379 s = ''
380 principals.each do |principal|
380 principals.each do |principal|
381 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
382 end
382 end
383 s.html_safe
383 s.html_safe
384 end
384 end
385
385
386 # Returns a string for users/groups option tags
386 # Returns a string for users/groups option tags
387 def principals_options_for_select(collection, selected=nil)
387 def principals_options_for_select(collection, selected=nil)
388 s = ''
388 s = ''
389 if collection.include?(User.current)
389 if collection.include?(User.current)
390 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
391 end
391 end
392 groups = ''
392 groups = ''
393 collection.sort.each do |element|
393 collection.sort.each do |element|
394 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
395 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
396 end
396 end
397 unless groups.empty?
397 unless groups.empty?
398 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
399 end
399 end
400 s.html_safe
400 s.html_safe
401 end
401 end
402
402
403 def option_tag(name, text, value, selected=nil, options={})
403 def option_tag(name, text, value, selected=nil, options={})
404 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
405 end
405 end
406
406
407 def truncate_single_line_raw(string, length)
407 def truncate_single_line_raw(string, length)
408 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
409 end
409 end
410
410
411 # Truncates at line break after 250 characters or options[:length]
411 # Truncates at line break after 250 characters or options[:length]
412 def truncate_lines(string, options={})
412 def truncate_lines(string, options={})
413 length = options[:length] || 250
413 length = options[:length] || 250
414 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 if string.to_s =~ /\A(.{#{length}}.*?)$/m
415 "#{$1}..."
415 "#{$1}..."
416 else
416 else
417 string
417 string
418 end
418 end
419 end
419 end
420
420
421 def anchor(text)
421 def anchor(text)
422 text.to_s.gsub(' ', '_')
422 text.to_s.gsub(' ', '_')
423 end
423 end
424
424
425 def html_hours(text)
425 def html_hours(text)
426 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
427 end
427 end
428
428
429 def authoring(created, author, options={})
429 def authoring(created, author, options={})
430 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
431 end
431 end
432
432
433 def time_tag(time)
433 def time_tag(time)
434 text = distance_of_time_in_words(Time.now, time)
434 text = distance_of_time_in_words(Time.now, time)
435 if @project
435 if @project
436 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
437 else
437 else
438 content_tag('abbr', text, :title => format_time(time))
438 content_tag('abbr', text, :title => format_time(time))
439 end
439 end
440 end
440 end
441
441
442 def syntax_highlight_lines(name, content)
442 def syntax_highlight_lines(name, content)
443 lines = []
443 lines = []
444 syntax_highlight(name, content).each_line { |line| lines << line }
444 syntax_highlight(name, content).each_line { |line| lines << line }
445 lines
445 lines
446 end
446 end
447
447
448 def syntax_highlight(name, content)
448 def syntax_highlight(name, content)
449 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
450 end
450 end
451
451
452 def to_path_param(path)
452 def to_path_param(path)
453 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
454 str.blank? ? nil : str
454 str.blank? ? nil : str
455 end
455 end
456
456
457 def reorder_links(name, url, method = :post)
457 def reorder_links(name, url, method = :post)
458 # TODO: remove associated styles from application.css too
458 # TODO: remove associated styles from application.css too
459 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
460
460
461 link_to(l(:label_sort_highest),
461 link_to(l(:label_sort_highest),
462 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
462 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
463 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
464 link_to(l(:label_sort_higher),
464 link_to(l(:label_sort_higher),
465 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
465 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
466 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
467 link_to(l(:label_sort_lower),
467 link_to(l(:label_sort_lower),
468 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
468 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
469 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
470 link_to(l(:label_sort_lowest),
470 link_to(l(:label_sort_lowest),
471 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
471 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
472 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
472 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
473 end
473 end
474
474
475 def reorder_handle(object, options={})
475 def reorder_handle(object, options={})
476 data = {
476 data = {
477 :reorder_url => options[:url] || url_for(object),
477 :reorder_url => options[:url] || url_for(object),
478 :reorder_param => options[:param] || object.class.name.underscore
478 :reorder_param => options[:param] || object.class.name.underscore
479 }
479 }
480 content_tag('span', '',
480 content_tag('span', '',
481 :class => "sort-handle",
481 :class => "sort-handle",
482 :data => data,
482 :data => data,
483 :title => l(:button_sort))
483 :title => l(:button_sort))
484 end
484 end
485
485
486 def breadcrumb(*args)
486 def breadcrumb(*args)
487 elements = args.flatten
487 elements = args.flatten
488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
489 end
489 end
490
490
491 def other_formats_links(&block)
491 def other_formats_links(&block)
492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
493 yield Redmine::Views::OtherFormatsBuilder.new(self)
493 yield Redmine::Views::OtherFormatsBuilder.new(self)
494 concat('</p>'.html_safe)
494 concat('</p>'.html_safe)
495 end
495 end
496
496
497 def page_header_title
497 def page_header_title
498 if @project.nil? || @project.new_record?
498 if @project.nil? || @project.new_record?
499 h(Setting.app_title)
499 h(Setting.app_title)
500 else
500 else
501 b = []
501 b = []
502 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
502 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
503 if ancestors.any?
503 if ancestors.any?
504 root = ancestors.shift
504 root = ancestors.shift
505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
506 if ancestors.size > 2
506 if ancestors.size > 2
507 b << "\xe2\x80\xa6"
507 b << "\xe2\x80\xa6"
508 ancestors = ancestors[-2, 2]
508 ancestors = ancestors[-2, 2]
509 end
509 end
510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
511 end
511 end
512 b << content_tag(:span, h(@project), class: 'current-project')
512 b << content_tag(:span, h(@project), class: 'current-project')
513 if b.size > 1
513 if b.size > 1
514 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
514 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
515 path = safe_join(b[0..-2], separator) + separator
515 path = safe_join(b[0..-2], separator) + separator
516 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
516 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
517 end
517 end
518 safe_join b
518 safe_join b
519 end
519 end
520 end
520 end
521
521
522 # Returns a h2 tag and sets the html title with the given arguments
522 # Returns a h2 tag and sets the html title with the given arguments
523 def title(*args)
523 def title(*args)
524 strings = args.map do |arg|
524 strings = args.map do |arg|
525 if arg.is_a?(Array) && arg.size >= 2
525 if arg.is_a?(Array) && arg.size >= 2
526 link_to(*arg)
526 link_to(*arg)
527 else
527 else
528 h(arg.to_s)
528 h(arg.to_s)
529 end
529 end
530 end
530 end
531 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
532 content_tag('h2', strings.join(' &#187; ').html_safe)
532 content_tag('h2', strings.join(' &#187; ').html_safe)
533 end
533 end
534
534
535 # Sets the html title
535 # Sets the html title
536 # Returns the html title when called without arguments
536 # Returns the html title when called without arguments
537 # Current project name and app_title and automatically appended
537 # Current project name and app_title and automatically appended
538 # Exemples:
538 # Exemples:
539 # html_title 'Foo', 'Bar'
539 # html_title 'Foo', 'Bar'
540 # html_title # => 'Foo - Bar - My Project - Redmine'
540 # html_title # => 'Foo - Bar - My Project - Redmine'
541 def html_title(*args)
541 def html_title(*args)
542 if args.empty?
542 if args.empty?
543 title = @html_title || []
543 title = @html_title || []
544 title << @project.name if @project
544 title << @project.name if @project
545 title << Setting.app_title unless Setting.app_title == title.last
545 title << Setting.app_title unless Setting.app_title == title.last
546 title.reject(&:blank?).join(' - ')
546 title.reject(&:blank?).join(' - ')
547 else
547 else
548 @html_title ||= []
548 @html_title ||= []
549 @html_title += args
549 @html_title += args
550 end
550 end
551 end
551 end
552
552
553 # Returns the theme, controller name, and action as css classes for the
553 # Returns the theme, controller name, and action as css classes for the
554 # HTML body.
554 # HTML body.
555 def body_css_classes
555 def body_css_classes
556 css = []
556 css = []
557 if theme = Redmine::Themes.theme(Setting.ui_theme)
557 if theme = Redmine::Themes.theme(Setting.ui_theme)
558 css << 'theme-' + theme.name
558 css << 'theme-' + theme.name
559 end
559 end
560
560
561 css << 'project-' + @project.identifier if @project && @project.identifier.present?
561 css << 'project-' + @project.identifier if @project && @project.identifier.present?
562 css << 'controller-' + controller_name
562 css << 'controller-' + controller_name
563 css << 'action-' + action_name
563 css << 'action-' + action_name
564 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
564 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
565 css << "textarea-#{User.current.pref.textarea_font}"
565 css << "textarea-#{User.current.pref.textarea_font}"
566 end
566 end
567 css.join(' ')
567 css.join(' ')
568 end
568 end
569
569
570 def accesskey(s)
570 def accesskey(s)
571 @used_accesskeys ||= []
571 @used_accesskeys ||= []
572 key = Redmine::AccessKeys.key_for(s)
572 key = Redmine::AccessKeys.key_for(s)
573 return nil if @used_accesskeys.include?(key)
573 return nil if @used_accesskeys.include?(key)
574 @used_accesskeys << key
574 @used_accesskeys << key
575 key
575 key
576 end
576 end
577
577
578 # Formats text according to system settings.
578 # Formats text according to system settings.
579 # 2 ways to call this method:
579 # 2 ways to call this method:
580 # * with a String: textilizable(text, options)
580 # * with a String: textilizable(text, options)
581 # * with an object and one of its attribute: textilizable(issue, :description, options)
581 # * with an object and one of its attribute: textilizable(issue, :description, options)
582 def textilizable(*args)
582 def textilizable(*args)
583 options = args.last.is_a?(Hash) ? args.pop : {}
583 options = args.last.is_a?(Hash) ? args.pop : {}
584 case args.size
584 case args.size
585 when 1
585 when 1
586 obj = options[:object]
586 obj = options[:object]
587 text = args.shift
587 text = args.shift
588 when 2
588 when 2
589 obj = args.shift
589 obj = args.shift
590 attr = args.shift
590 attr = args.shift
591 text = obj.send(attr).to_s
591 text = obj.send(attr).to_s
592 else
592 else
593 raise ArgumentError, 'invalid arguments to textilizable'
593 raise ArgumentError, 'invalid arguments to textilizable'
594 end
594 end
595 return '' if text.blank?
595 return '' if text.blank?
596 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
596 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
597 @only_path = only_path = options.delete(:only_path) == false ? false : true
597 @only_path = only_path = options.delete(:only_path) == false ? false : true
598
598
599 text = text.dup
599 text = text.dup
600 macros = catch_macros(text)
600 macros = catch_macros(text)
601 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
601 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
602
602
603 @parsed_headings = []
603 @parsed_headings = []
604 @heading_anchors = {}
604 @heading_anchors = {}
605 @current_section = 0 if options[:edit_section_links]
605 @current_section = 0 if options[:edit_section_links]
606
606
607 parse_sections(text, project, obj, attr, only_path, options)
607 parse_sections(text, project, obj, attr, only_path, options)
608 text = parse_non_pre_blocks(text, obj, macros) do |text|
608 text = parse_non_pre_blocks(text, obj, macros) do |text|
609 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
609 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
610 send method_name, text, project, obj, attr, only_path, options
610 send method_name, text, project, obj, attr, only_path, options
611 end
611 end
612 end
612 end
613 parse_headings(text, project, obj, attr, only_path, options)
613 parse_headings(text, project, obj, attr, only_path, options)
614
614
615 if @parsed_headings.any?
615 if @parsed_headings.any?
616 replace_toc(text, @parsed_headings)
616 replace_toc(text, @parsed_headings)
617 end
617 end
618
618
619 text.html_safe
619 text.html_safe
620 end
620 end
621
621
622 def parse_non_pre_blocks(text, obj, macros)
622 def parse_non_pre_blocks(text, obj, macros)
623 s = StringScanner.new(text)
623 s = StringScanner.new(text)
624 tags = []
624 tags = []
625 parsed = ''
625 parsed = ''
626 while !s.eos?
626 while !s.eos?
627 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
627 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
628 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
628 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
629 if tags.empty?
629 if tags.empty?
630 yield text
630 yield text
631 inject_macros(text, obj, macros) if macros.any?
631 inject_macros(text, obj, macros) if macros.any?
632 else
632 else
633 inject_macros(text, obj, macros, false) if macros.any?
633 inject_macros(text, obj, macros, false) if macros.any?
634 end
634 end
635 parsed << text
635 parsed << text
636 if tag
636 if tag
637 if closing
637 if closing
638 if tags.last && tags.last.casecmp(tag) == 0
638 if tags.last && tags.last.casecmp(tag) == 0
639 tags.pop
639 tags.pop
640 end
640 end
641 else
641 else
642 tags << tag.downcase
642 tags << tag.downcase
643 end
643 end
644 parsed << full_tag
644 parsed << full_tag
645 end
645 end
646 end
646 end
647 # Close any non closing tags
647 # Close any non closing tags
648 while tag = tags.pop
648 while tag = tags.pop
649 parsed << "</#{tag}>"
649 parsed << "</#{tag}>"
650 end
650 end
651 parsed
651 parsed
652 end
652 end
653
653
654 def parse_inline_attachments(text, project, obj, attr, only_path, options)
654 def parse_inline_attachments(text, project, obj, attr, only_path, options)
655 return if options[:inline_attachments] == false
655 return if options[:inline_attachments] == false
656
656
657 # when using an image link, try to use an attachment, if possible
657 # when using an image link, try to use an attachment, if possible
658 attachments = options[:attachments] || []
658 attachments = options[:attachments] || []
659 attachments += obj.attachments if obj.respond_to?(:attachments)
659 attachments += obj.attachments if obj.respond_to?(:attachments)
660 if attachments.present?
660 if attachments.present?
661 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
661 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
662 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
662 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
663 # search for the picture in attachments
663 # search for the picture in attachments
664 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
664 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
665 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
665 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
666 desc = found.description.to_s.gsub('"', '')
666 desc = found.description.to_s.gsub('"', '')
667 if !desc.blank? && alttext.blank?
667 if !desc.blank? && alttext.blank?
668 alt = " title=\"#{desc}\" alt=\"#{desc}\""
668 alt = " title=\"#{desc}\" alt=\"#{desc}\""
669 end
669 end
670 "src=\"#{image_url}\"#{alt}"
670 "src=\"#{image_url}\"#{alt}"
671 else
671 else
672 m
672 m
673 end
673 end
674 end
674 end
675 end
675 end
676 end
676 end
677
677
678 # Wiki links
678 # Wiki links
679 #
679 #
680 # Examples:
680 # Examples:
681 # [[mypage]]
681 # [[mypage]]
682 # [[mypage|mytext]]
682 # [[mypage|mytext]]
683 # wiki links can refer other project wikis, using project name or identifier:
683 # wiki links can refer other project wikis, using project name or identifier:
684 # [[project:]] -> wiki starting page
684 # [[project:]] -> wiki starting page
685 # [[project:|mytext]]
685 # [[project:|mytext]]
686 # [[project:mypage]]
686 # [[project:mypage]]
687 # [[project:mypage|mytext]]
687 # [[project:mypage|mytext]]
688 def parse_wiki_links(text, project, obj, attr, only_path, options)
688 def parse_wiki_links(text, project, obj, attr, only_path, options)
689 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
689 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
690 link_project = project
690 link_project = project
691 esc, all, page, title = $1, $2, $3, $5
691 esc, all, page, title = $1, $2, $3, $5
692 if esc.nil?
692 if esc.nil?
693 if page =~ /^([^\:]+)\:(.*)$/
693 if page =~ /^([^\:]+)\:(.*)$/
694 identifier, page = $1, $2
694 identifier, page = $1, $2
695 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
695 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
696 title ||= identifier if page.blank?
696 title ||= identifier if page.blank?
697 end
697 end
698
698
699 if link_project && link_project.wiki
699 if link_project && link_project.wiki
700 # extract anchor
700 # extract anchor
701 anchor = nil
701 anchor = nil
702 if page =~ /^(.+?)\#(.+)$/
702 if page =~ /^(.+?)\#(.+)$/
703 page, anchor = $1, $2
703 page, anchor = $1, $2
704 end
704 end
705 anchor = sanitize_anchor_name(anchor) if anchor.present?
705 anchor = sanitize_anchor_name(anchor) if anchor.present?
706 # check if page exists
706 # check if page exists
707 wiki_page = link_project.wiki.find_page(page)
707 wiki_page = link_project.wiki.find_page(page)
708 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
708 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
709 "##{anchor}"
709 "##{anchor}"
710 else
710 else
711 case options[:wiki_links]
711 case options[:wiki_links]
712 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
712 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
713 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
713 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
714 else
714 else
715 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
715 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
716 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
716 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
717 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
717 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
718 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
718 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
719 end
719 end
720 end
720 end
721 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
721 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
722 else
722 else
723 # project or wiki doesn't exist
723 # project or wiki doesn't exist
724 all
724 all
725 end
725 end
726 else
726 else
727 all
727 all
728 end
728 end
729 end
729 end
730 end
730 end
731
731
732 # Redmine links
732 # Redmine links
733 #
733 #
734 # Examples:
734 # Examples:
735 # Issues:
735 # Issues:
736 # #52 -> Link to issue #52
736 # #52 -> Link to issue #52
737 # Changesets:
737 # Changesets:
738 # r52 -> Link to revision 52
738 # r52 -> Link to revision 52
739 # commit:a85130f -> Link to scmid starting with a85130f
739 # commit:a85130f -> Link to scmid starting with a85130f
740 # Documents:
740 # Documents:
741 # document#17 -> Link to document with id 17
741 # document#17 -> Link to document with id 17
742 # document:Greetings -> Link to the document with title "Greetings"
742 # document:Greetings -> Link to the document with title "Greetings"
743 # document:"Some document" -> Link to the document with title "Some document"
743 # document:"Some document" -> Link to the document with title "Some document"
744 # Versions:
744 # Versions:
745 # version#3 -> Link to version with id 3
745 # version#3 -> Link to version with id 3
746 # version:1.0.0 -> Link to version named "1.0.0"
746 # version:1.0.0 -> Link to version named "1.0.0"
747 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
747 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
748 # Attachments:
748 # Attachments:
749 # attachment:file.zip -> Link to the attachment of the current object named file.zip
749 # attachment:file.zip -> Link to the attachment of the current object named file.zip
750 # Source files:
750 # Source files:
751 # source:some/file -> Link to the file located at /some/file in the project's repository
751 # source:some/file -> Link to the file located at /some/file in the project's repository
752 # source:some/file@52 -> Link to the file's revision 52
752 # source:some/file@52 -> Link to the file's revision 52
753 # source:some/file#L120 -> Link to line 120 of the file
753 # source:some/file#L120 -> Link to line 120 of the file
754 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
754 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
755 # export:some/file -> Force the download of the file
755 # export:some/file -> Force the download of the file
756 # Forum messages:
756 # Forum messages:
757 # message#1218 -> Link to message with id 1218
757 # message#1218 -> Link to message with id 1218
758 # Projects:
758 # Projects:
759 # project:someproject -> Link to project named "someproject"
759 # project:someproject -> Link to project named "someproject"
760 # project#3 -> Link to project with id 3
760 # project#3 -> Link to project with id 3
761 #
761 #
762 # Links can refer other objects from other projects, using project identifier:
762 # Links can refer other objects from other projects, using project identifier:
763 # identifier:r52
763 # identifier:r52
764 # identifier:document:"Some document"
764 # identifier:document:"Some document"
765 # identifier:version:1.0.0
765 # identifier:version:1.0.0
766 # identifier:source:some/file
766 # identifier:source:some/file
767 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
767 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
768 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
768 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
769 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
769 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
770 if tag_content
770 if tag_content
771 $&
771 $&
772 else
772 else
773 link = nil
773 link = nil
774 project = default_project
774 project = default_project
775 if project_identifier
775 if project_identifier
776 project = Project.visible.find_by_identifier(project_identifier)
776 project = Project.visible.find_by_identifier(project_identifier)
777 end
777 end
778 if esc.nil?
778 if esc.nil?
779 if prefix.nil? && sep == 'r'
779 if prefix.nil? && sep == 'r'
780 if project
780 if project
781 repository = nil
781 repository = nil
782 if repo_identifier
782 if repo_identifier
783 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
783 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
784 else
784 else
785 repository = project.repository
785 repository = project.repository
786 end
786 end
787 # project.changesets.visible raises an SQL error because of a double join on repositories
787 # project.changesets.visible raises an SQL error because of a double join on repositories
788 if repository &&
788 if repository &&
789 (changeset = Changeset.visible.
789 (changeset = Changeset.visible.
790 find_by_repository_id_and_revision(repository.id, identifier))
790 find_by_repository_id_and_revision(repository.id, identifier))
791 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
791 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
792 {:only_path => only_path, :controller => 'repositories',
792 {:only_path => only_path, :controller => 'repositories',
793 :action => 'revision', :id => project,
793 :action => 'revision', :id => project,
794 :repository_id => repository.identifier_param,
794 :repository_id => repository.identifier_param,
795 :rev => changeset.revision},
795 :rev => changeset.revision},
796 :class => 'changeset',
796 :class => 'changeset',
797 :title => truncate_single_line_raw(changeset.comments, 100))
797 :title => truncate_single_line_raw(changeset.comments, 100))
798 end
798 end
799 end
799 end
800 elsif sep == '#'
800 elsif sep == '#'
801 oid = identifier.to_i
801 oid = identifier.to_i
802 case prefix
802 case prefix
803 when nil
803 when nil
804 if oid.to_s == identifier &&
804 if oid.to_s == identifier &&
805 issue = Issue.visible.find_by_id(oid)
805 issue = Issue.visible.find_by_id(oid)
806 anchor = comment_id ? "note-#{comment_id}" : nil
806 anchor = comment_id ? "note-#{comment_id}" : nil
807 link = link_to("##{oid}#{comment_suffix}",
807 link = link_to("##{oid}#{comment_suffix}",
808 issue_url(issue, :only_path => only_path, :anchor => anchor),
808 issue_url(issue, :only_path => only_path, :anchor => anchor),
809 :class => issue.css_classes,
809 :class => issue.css_classes,
810 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
810 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
811 end
811 end
812 when 'document'
812 when 'document'
813 if document = Document.visible.find_by_id(oid)
813 if document = Document.visible.find_by_id(oid)
814 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
814 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
815 end
815 end
816 when 'version'
816 when 'version'
817 if version = Version.visible.find_by_id(oid)
817 if version = Version.visible.find_by_id(oid)
818 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
818 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
819 end
819 end
820 when 'message'
820 when 'message'
821 if message = Message.visible.find_by_id(oid)
821 if message = Message.visible.find_by_id(oid)
822 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
822 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
823 end
823 end
824 when 'forum'
824 when 'forum'
825 if board = Board.visible.find_by_id(oid)
825 if board = Board.visible.find_by_id(oid)
826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
827 end
827 end
828 when 'news'
828 when 'news'
829 if news = News.visible.find_by_id(oid)
829 if news = News.visible.find_by_id(oid)
830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
831 end
831 end
832 when 'project'
832 when 'project'
833 if p = Project.visible.find_by_id(oid)
833 if p = Project.visible.find_by_id(oid)
834 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
834 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
835 end
835 end
836 end
836 end
837 elsif sep == ':'
837 elsif sep == ':'
838 # removes the double quotes if any
838 # removes the double quotes if any
839 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
839 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
840 name = CGI.unescapeHTML(name)
840 name = CGI.unescapeHTML(name)
841 case prefix
841 case prefix
842 when 'document'
842 when 'document'
843 if project && document = project.documents.visible.find_by_title(name)
843 if project && document = project.documents.visible.find_by_title(name)
844 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
844 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
845 end
845 end
846 when 'version'
846 when 'version'
847 if project && version = project.versions.visible.find_by_name(name)
847 if project && version = project.versions.visible.find_by_name(name)
848 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
848 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
849 end
849 end
850 when 'forum'
850 when 'forum'
851 if project && board = project.boards.visible.find_by_name(name)
851 if project && board = project.boards.visible.find_by_name(name)
852 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
852 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
853 end
853 end
854 when 'news'
854 when 'news'
855 if project && news = project.news.visible.find_by_title(name)
855 if project && news = project.news.visible.find_by_title(name)
856 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
856 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
857 end
857 end
858 when 'commit', 'source', 'export'
858 when 'commit', 'source', 'export'
859 if project
859 if project
860 repository = nil
860 repository = nil
861 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
861 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
862 repo_prefix, repo_identifier, name = $1, $2, $3
862 repo_prefix, repo_identifier, name = $1, $2, $3
863 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
863 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
864 else
864 else
865 repository = project.repository
865 repository = project.repository
866 end
866 end
867 if prefix == 'commit'
867 if prefix == 'commit'
868 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
868 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
869 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
869 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
870 :class => 'changeset',
870 :class => 'changeset',
871 :title => truncate_single_line_raw(changeset.comments, 100)
871 :title => truncate_single_line_raw(changeset.comments, 100)
872 end
872 end
873 else
873 else
874 if repository && User.current.allowed_to?(:browse_repository, project)
874 if repository && User.current.allowed_to?(:browse_repository, project)
875 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
875 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
876 path, rev, anchor = $1, $3, $5
876 path, rev, anchor = $1, $3, $5
877 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
877 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
878 :path => to_path_param(path),
878 :path => to_path_param(path),
879 :rev => rev,
879 :rev => rev,
880 :anchor => anchor},
880 :anchor => anchor},
881 :class => (prefix == 'export' ? 'source download' : 'source')
881 :class => (prefix == 'export' ? 'source download' : 'source')
882 end
882 end
883 end
883 end
884 repo_prefix = nil
884 repo_prefix = nil
885 end
885 end
886 when 'attachment'
886 when 'attachment'
887 attachments = options[:attachments] || []
887 attachments = options[:attachments] || []
888 attachments += obj.attachments if obj.respond_to?(:attachments)
888 attachments += obj.attachments if obj.respond_to?(:attachments)
889 if attachments && attachment = Attachment.latest_attach(attachments, name)
889 if attachments && attachment = Attachment.latest_attach(attachments, name)
890 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
890 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
891 end
891 end
892 when 'project'
892 when 'project'
893 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
893 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
894 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
894 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
895 end
895 end
896 end
896 end
897 end
897 end
898 end
898 end
899 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
899 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
900 end
900 end
901 end
901 end
902 end
902 end
903
903
904 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
904 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
905
905
906 def parse_sections(text, project, obj, attr, only_path, options)
906 def parse_sections(text, project, obj, attr, only_path, options)
907 return unless options[:edit_section_links]
907 return unless options[:edit_section_links]
908 text.gsub!(HEADING_RE) do
908 text.gsub!(HEADING_RE) do
909 heading, level = $1, $2
909 heading, level = $1, $2
910 @current_section += 1
910 @current_section += 1
911 if @current_section > 1
911 if @current_section > 1
912 content_tag('div',
912 content_tag('div',
913 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
913 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
914 :class => 'icon-only icon-edit'),
914 :class => 'icon-only icon-edit'),
915 :class => "contextual heading-#{level}",
915 :class => "contextual heading-#{level}",
916 :title => l(:button_edit_section),
916 :title => l(:button_edit_section),
917 :id => "section-#{@current_section}") + heading.html_safe
917 :id => "section-#{@current_section}") + heading.html_safe
918 else
918 else
919 heading
919 heading
920 end
920 end
921 end
921 end
922 end
922 end
923
923
924 # Headings and TOC
924 # Headings and TOC
925 # Adds ids and links to headings unless options[:headings] is set to false
925 # Adds ids and links to headings unless options[:headings] is set to false
926 def parse_headings(text, project, obj, attr, only_path, options)
926 def parse_headings(text, project, obj, attr, only_path, options)
927 return if options[:headings] == false
927 return if options[:headings] == false
928
928
929 text.gsub!(HEADING_RE) do
929 text.gsub!(HEADING_RE) do
930 level, attrs, content = $2.to_i, $3, $4
930 level, attrs, content = $2.to_i, $3, $4
931 item = strip_tags(content).strip
931 item = strip_tags(content).strip
932 anchor = sanitize_anchor_name(item)
932 anchor = sanitize_anchor_name(item)
933 # used for single-file wiki export
933 # used for single-file wiki export
934 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
934 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
935 @heading_anchors[anchor] ||= 0
935 @heading_anchors[anchor] ||= 0
936 idx = (@heading_anchors[anchor] += 1)
936 idx = (@heading_anchors[anchor] += 1)
937 if idx > 1
937 if idx > 1
938 anchor = "#{anchor}-#{idx}"
938 anchor = "#{anchor}-#{idx}"
939 end
939 end
940 @parsed_headings << [level, anchor, item]
940 @parsed_headings << [level, anchor, item]
941 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
941 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
942 end
942 end
943 end
943 end
944
944
945 MACROS_RE = /(
945 MACROS_RE = /(
946 (!)? # escaping
946 (!)? # escaping
947 (
947 (
948 \{\{ # opening tag
948 \{\{ # opening tag
949 ([\w]+) # macro name
949 ([\w]+) # macro name
950 (\(([^\n\r]*?)\))? # optional arguments
950 (\(([^\n\r]*?)\))? # optional arguments
951 ([\n\r].*?[\n\r])? # optional block of text
951 ([\n\r].*?[\n\r])? # optional block of text
952 \}\} # closing tag
952 \}\} # closing tag
953 )
953 )
954 )/mx unless const_defined?(:MACROS_RE)
954 )/mx unless const_defined?(:MACROS_RE)
955
955
956 MACRO_SUB_RE = /(
956 MACRO_SUB_RE = /(
957 \{\{
957 \{\{
958 macro\((\d+)\)
958 macro\((\d+)\)
959 \}\}
959 \}\}
960 )/x unless const_defined?(:MACRO_SUB_RE)
960 )/x unless const_defined?(:MACRO_SUB_RE)
961
961
962 # Extracts macros from text
962 # Extracts macros from text
963 def catch_macros(text)
963 def catch_macros(text)
964 macros = {}
964 macros = {}
965 text.gsub!(MACROS_RE) do
965 text.gsub!(MACROS_RE) do
966 all, macro = $1, $4.downcase
966 all, macro = $1, $4.downcase
967 if macro_exists?(macro) || all =~ MACRO_SUB_RE
967 if macro_exists?(macro) || all =~ MACRO_SUB_RE
968 index = macros.size
968 index = macros.size
969 macros[index] = all
969 macros[index] = all
970 "{{macro(#{index})}}"
970 "{{macro(#{index})}}"
971 else
971 else
972 all
972 all
973 end
973 end
974 end
974 end
975 macros
975 macros
976 end
976 end
977
977
978 # Executes and replaces macros in text
978 # Executes and replaces macros in text
979 def inject_macros(text, obj, macros, execute=true)
979 def inject_macros(text, obj, macros, execute=true)
980 text.gsub!(MACRO_SUB_RE) do
980 text.gsub!(MACRO_SUB_RE) do
981 all, index = $1, $2.to_i
981 all, index = $1, $2.to_i
982 orig = macros.delete(index)
982 orig = macros.delete(index)
983 if execute && orig && orig =~ MACROS_RE
983 if execute && orig && orig =~ MACROS_RE
984 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
984 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
985 if esc.nil?
985 if esc.nil?
986 h(exec_macro(macro, obj, args, block) || all)
986 h(exec_macro(macro, obj, args, block) || all)
987 else
987 else
988 h(all)
988 h(all)
989 end
989 end
990 elsif orig
990 elsif orig
991 h(orig)
991 h(orig)
992 else
992 else
993 h(all)
993 h(all)
994 end
994 end
995 end
995 end
996 end
996 end
997
997
998 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
998 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
999
999
1000 # Renders the TOC with given headings
1000 # Renders the TOC with given headings
1001 def replace_toc(text, headings)
1001 def replace_toc(text, headings)
1002 text.gsub!(TOC_RE) do
1002 text.gsub!(TOC_RE) do
1003 left_align, right_align = $2, $3
1003 left_align, right_align = $2, $3
1004 # Keep only the 4 first levels
1004 # Keep only the 4 first levels
1005 headings = headings.select{|level, anchor, item| level <= 4}
1005 headings = headings.select{|level, anchor, item| level <= 4}
1006 if headings.empty?
1006 if headings.empty?
1007 ''
1007 ''
1008 else
1008 else
1009 div_class = 'toc'
1009 div_class = 'toc'
1010 div_class << ' right' if right_align
1010 div_class << ' right' if right_align
1011 div_class << ' left' if left_align
1011 div_class << ' left' if left_align
1012 out = "<ul class=\"#{div_class}\"><li>"
1012 out = "<ul class=\"#{div_class}\"><li>"
1013 root = headings.map(&:first).min
1013 root = headings.map(&:first).min
1014 current = root
1014 current = root
1015 started = false
1015 started = false
1016 headings.each do |level, anchor, item|
1016 headings.each do |level, anchor, item|
1017 if level > current
1017 if level > current
1018 out << '<ul><li>' * (level - current)
1018 out << '<ul><li>' * (level - current)
1019 elsif level < current
1019 elsif level < current
1020 out << "</li></ul>\n" * (current - level) + "</li><li>"
1020 out << "</li></ul>\n" * (current - level) + "</li><li>"
1021 elsif started
1021 elsif started
1022 out << '</li><li>'
1022 out << '</li><li>'
1023 end
1023 end
1024 out << "<a href=\"##{anchor}\">#{item}</a>"
1024 out << "<a href=\"##{anchor}\">#{item}</a>"
1025 current = level
1025 current = level
1026 started = true
1026 started = true
1027 end
1027 end
1028 out << '</li></ul>' * (current - root)
1028 out << '</li></ul>' * (current - root)
1029 out << '</li></ul>'
1029 out << '</li></ul>'
1030 end
1030 end
1031 end
1031 end
1032 end
1032 end
1033
1033
1034 # Same as Rails' simple_format helper without using paragraphs
1034 # Same as Rails' simple_format helper without using paragraphs
1035 def simple_format_without_paragraph(text)
1035 def simple_format_without_paragraph(text)
1036 text.to_s.
1036 text.to_s.
1037 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1037 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1038 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1038 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1039 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1039 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1040 html_safe
1040 html_safe
1041 end
1041 end
1042
1042
1043 def lang_options_for_select(blank=true)
1043 def lang_options_for_select(blank=true)
1044 (blank ? [["(auto)", ""]] : []) + languages_options
1044 (blank ? [["(auto)", ""]] : []) + languages_options
1045 end
1045 end
1046
1046
1047 def labelled_form_for(*args, &proc)
1047 def labelled_form_for(*args, &proc)
1048 args << {} unless args.last.is_a?(Hash)
1048 args << {} unless args.last.is_a?(Hash)
1049 options = args.last
1049 options = args.last
1050 if args.first.is_a?(Symbol)
1050 if args.first.is_a?(Symbol)
1051 options.merge!(:as => args.shift)
1051 options.merge!(:as => args.shift)
1052 end
1052 end
1053 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1053 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1054 form_for(*args, &proc)
1054 form_for(*args, &proc)
1055 end
1055 end
1056
1056
1057 def labelled_fields_for(*args, &proc)
1057 def labelled_fields_for(*args, &proc)
1058 args << {} unless args.last.is_a?(Hash)
1058 args << {} unless args.last.is_a?(Hash)
1059 options = args.last
1059 options = args.last
1060 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1060 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1061 fields_for(*args, &proc)
1061 fields_for(*args, &proc)
1062 end
1062 end
1063
1063
1064 # Render the error messages for the given objects
1064 # Render the error messages for the given objects
1065 def error_messages_for(*objects)
1065 def error_messages_for(*objects)
1066 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1066 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1067 errors = objects.map {|o| o.errors.full_messages}.flatten
1067 errors = objects.map {|o| o.errors.full_messages}.flatten
1068 render_error_messages(errors)
1068 render_error_messages(errors)
1069 end
1069 end
1070
1070
1071 # Renders a list of error messages
1071 # Renders a list of error messages
1072 def render_error_messages(errors)
1072 def render_error_messages(errors)
1073 html = ""
1073 html = ""
1074 if errors.present?
1074 if errors.present?
1075 html << "<div id='errorExplanation'><ul>\n"
1075 html << "<div id='errorExplanation'><ul>\n"
1076 errors.each do |error|
1076 errors.each do |error|
1077 html << "<li>#{h error}</li>\n"
1077 html << "<li>#{h error}</li>\n"
1078 end
1078 end
1079 html << "</ul></div>\n"
1079 html << "</ul></div>\n"
1080 end
1080 end
1081 html.html_safe
1081 html.html_safe
1082 end
1082 end
1083
1083
1084 def delete_link(url, options={})
1084 def delete_link(url, options={})
1085 options = {
1085 options = {
1086 :method => :delete,
1086 :method => :delete,
1087 :data => {:confirm => l(:text_are_you_sure)},
1087 :data => {:confirm => l(:text_are_you_sure)},
1088 :class => 'icon icon-del'
1088 :class => 'icon icon-del'
1089 }.merge(options)
1089 }.merge(options)
1090
1090
1091 link_to l(:button_delete), url, options
1091 link_to l(:button_delete), url, options
1092 end
1092 end
1093
1093
1094 def preview_link(url, form, target='preview', options={})
1094 def preview_link(url, form, target='preview', options={})
1095 content_tag 'a', l(:label_preview), {
1095 content_tag 'a', l(:label_preview), {
1096 :href => "#",
1096 :href => "#",
1097 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1097 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1098 :accesskey => accesskey(:preview)
1098 :accesskey => accesskey(:preview)
1099 }.merge(options)
1099 }.merge(options)
1100 end
1100 end
1101
1101
1102 def link_to_function(name, function, html_options={})
1102 def link_to_function(name, function, html_options={})
1103 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1103 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1104 end
1104 end
1105
1105
1106 # Helper to render JSON in views
1106 # Helper to render JSON in views
1107 def raw_json(arg)
1107 def raw_json(arg)
1108 arg.to_json.to_s.gsub('/', '\/').html_safe
1108 arg.to_json.to_s.gsub('/', '\/').html_safe
1109 end
1109 end
1110
1110
1111 def back_url
1111 def back_url
1112 url = params[:back_url]
1112 url = params[:back_url]
1113 if url.nil? && referer = request.env['HTTP_REFERER']
1113 if url.nil? && referer = request.env['HTTP_REFERER']
1114 url = CGI.unescape(referer.to_s)
1114 url = CGI.unescape(referer.to_s)
1115 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1115 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1116 # parsed as invalid by URI.parse so the redirect to the back URL would
1116 # parsed as invalid by URI.parse so the redirect to the back URL would
1117 # not be accepted (ApplicationController#validate_back_url would return
1117 # not be accepted (ApplicationController#validate_back_url would return
1118 # false)
1118 # false)
1119 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1119 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1120 end
1120 end
1121 url
1121 url
1122 end
1122 end
1123
1123
1124 def back_url_hidden_field_tag
1124 def back_url_hidden_field_tag
1125 url = back_url
1125 url = back_url
1126 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1126 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1127 end
1127 end
1128
1128
1129 def check_all_links(form_name)
1129 def check_all_links(form_name)
1130 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1130 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1131 " | ".html_safe +
1131 " | ".html_safe +
1132 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1132 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1133 end
1133 end
1134
1134
1135 def toggle_checkboxes_link(selector)
1135 def toggle_checkboxes_link(selector)
1136 link_to_function '',
1136 link_to_function '',
1137 "toggleCheckboxesBySelector('#{selector}')",
1137 "toggleCheckboxesBySelector('#{selector}')",
1138 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1138 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1139 :class => 'toggle-checkboxes'
1139 :class => 'toggle-checkboxes'
1140 end
1140 end
1141
1141
1142 def progress_bar(pcts, options={})
1142 def progress_bar(pcts, options={})
1143 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1143 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1144 pcts = pcts.collect(&:round)
1144 pcts = pcts.collect(&:round)
1145 pcts[1] = pcts[1] - pcts[0]
1145 pcts[1] = pcts[1] - pcts[0]
1146 pcts << (100 - pcts[1] - pcts[0])
1146 pcts << (100 - pcts[1] - pcts[0])
1147 titles = options[:titles].to_a
1147 titles = options[:titles].to_a
1148 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1148 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1149 legend = options[:legend] || ''
1149 legend = options[:legend] || ''
1150 content_tag('table',
1150 content_tag('table',
1151 content_tag('tr',
1151 content_tag('tr',
1152 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1152 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1153 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1153 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1154 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1154 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1155 ), :class => "progress progress-#{pcts[0]}").html_safe +
1155 ), :class => "progress progress-#{pcts[0]}").html_safe +
1156 content_tag('p', legend, :class => 'percent').html_safe
1156 content_tag('p', legend, :class => 'percent').html_safe
1157 end
1157 end
1158
1158
1159 def checked_image(checked=true)
1159 def checked_image(checked=true)
1160 if checked
1160 if checked
1161 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1161 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1162 end
1162 end
1163 end
1163 end
1164
1164
1165 def context_menu(url)
1165 def context_menu(url)
1166 unless @context_menu_included
1166 unless @context_menu_included
1167 content_for :header_tags do
1167 content_for :header_tags do
1168 javascript_include_tag('context_menu') +
1168 javascript_include_tag('context_menu') +
1169 stylesheet_link_tag('context_menu')
1169 stylesheet_link_tag('context_menu')
1170 end
1170 end
1171 if l(:direction) == 'rtl'
1171 if l(:direction) == 'rtl'
1172 content_for :header_tags do
1172 content_for :header_tags do
1173 stylesheet_link_tag('context_menu_rtl')
1173 stylesheet_link_tag('context_menu_rtl')
1174 end
1174 end
1175 end
1175 end
1176 @context_menu_included = true
1176 @context_menu_included = true
1177 end
1177 end
1178 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1178 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1179 end
1179 end
1180
1180
1181 def calendar_for(field_id)
1181 def calendar_for(field_id)
1182 include_calendar_headers_tags
1182 include_calendar_headers_tags
1183 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1183 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1184 end
1184 end
1185
1185
1186 def include_calendar_headers_tags
1186 def include_calendar_headers_tags
1187 unless @calendar_headers_tags_included
1187 unless @calendar_headers_tags_included
1188 tags = ''.html_safe
1188 tags = ''.html_safe
1189 @calendar_headers_tags_included = true
1189 @calendar_headers_tags_included = true
1190 content_for :header_tags do
1190 content_for :header_tags do
1191 start_of_week = Setting.start_of_week
1191 start_of_week = Setting.start_of_week
1192 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1192 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1193 # Redmine uses 1..7 (monday..sunday) in settings and locales
1193 # Redmine uses 1..7 (monday..sunday) in settings and locales
1194 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1194 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1195 start_of_week = start_of_week.to_i % 7
1195 start_of_week = start_of_week.to_i % 7
1196 tags << javascript_tag(
1196 tags << javascript_tag(
1197 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1197 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1198 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1198 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1199 path_to_image('/images/calendar.png') +
1199 path_to_image('/images/calendar.png') +
1200 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1200 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1201 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1201 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1202 "beforeShow: beforeShowDatePicker};")
1202 "beforeShow: beforeShowDatePicker};")
1203 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1203 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1204 unless jquery_locale == 'en'
1204 unless jquery_locale == 'en'
1205 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1205 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1206 end
1206 end
1207 tags
1207 tags
1208 end
1208 end
1209 end
1209 end
1210 end
1210 end
1211
1211
1212 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1212 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1213 # Examples:
1213 # Examples:
1214 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1214 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1215 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1215 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1216 #
1216 #
1217 def stylesheet_link_tag(*sources)
1217 def stylesheet_link_tag(*sources)
1218 options = sources.last.is_a?(Hash) ? sources.pop : {}
1218 options = sources.last.is_a?(Hash) ? sources.pop : {}
1219 plugin = options.delete(:plugin)
1219 plugin = options.delete(:plugin)
1220 sources = sources.map do |source|
1220 sources = sources.map do |source|
1221 if plugin
1221 if plugin
1222 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1222 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1223 elsif current_theme && current_theme.stylesheets.include?(source)
1223 elsif current_theme && current_theme.stylesheets.include?(source)
1224 current_theme.stylesheet_path(source)
1224 current_theme.stylesheet_path(source)
1225 else
1225 else
1226 source
1226 source
1227 end
1227 end
1228 end
1228 end
1229 super *sources, options
1229 super *sources, options
1230 end
1230 end
1231
1231
1232 # Overrides Rails' image_tag with themes and plugins support.
1232 # Overrides Rails' image_tag with themes and plugins support.
1233 # Examples:
1233 # Examples:
1234 # image_tag('image.png') # => picks image.png from the current theme or defaults
1234 # image_tag('image.png') # => picks image.png from the current theme or defaults
1235 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1235 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1236 #
1236 #
1237 def image_tag(source, options={})
1237 def image_tag(source, options={})
1238 if plugin = options.delete(:plugin)
1238 if plugin = options.delete(:plugin)
1239 source = "/plugin_assets/#{plugin}/images/#{source}"
1239 source = "/plugin_assets/#{plugin}/images/#{source}"
1240 elsif current_theme && current_theme.images.include?(source)
1240 elsif current_theme && current_theme.images.include?(source)
1241 source = current_theme.image_path(source)
1241 source = current_theme.image_path(source)
1242 end
1242 end
1243 super source, options
1243 super source, options
1244 end
1244 end
1245
1245
1246 # Overrides Rails' javascript_include_tag with plugins support
1246 # Overrides Rails' javascript_include_tag with plugins support
1247 # Examples:
1247 # Examples:
1248 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1248 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1249 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1249 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1250 #
1250 #
1251 def javascript_include_tag(*sources)
1251 def javascript_include_tag(*sources)
1252 options = sources.last.is_a?(Hash) ? sources.pop : {}
1252 options = sources.last.is_a?(Hash) ? sources.pop : {}
1253 if plugin = options.delete(:plugin)
1253 if plugin = options.delete(:plugin)
1254 sources = sources.map do |source|
1254 sources = sources.map do |source|
1255 if plugin
1255 if plugin
1256 "/plugin_assets/#{plugin}/javascripts/#{source}"
1256 "/plugin_assets/#{plugin}/javascripts/#{source}"
1257 else
1257 else
1258 source
1258 source
1259 end
1259 end
1260 end
1260 end
1261 end
1261 end
1262 super *sources, options
1262 super *sources, options
1263 end
1263 end
1264
1264
1265 def sidebar_content?
1265 def sidebar_content?
1266 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1266 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1267 end
1267 end
1268
1268
1269 def view_layouts_base_sidebar_hook_response
1269 def view_layouts_base_sidebar_hook_response
1270 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1270 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1271 end
1271 end
1272
1272
1273 def email_delivery_enabled?
1273 def email_delivery_enabled?
1274 !!ActionMailer::Base.perform_deliveries
1274 !!ActionMailer::Base.perform_deliveries
1275 end
1275 end
1276
1276
1277 # Returns the avatar image tag for the given +user+ if avatars are enabled
1277 # Returns the avatar image tag for the given +user+ if avatars are enabled
1278 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1278 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1279 def avatar(user, options = { })
1279 def avatar(user, options = { })
1280 if Setting.gravatar_enabled?
1280 if Setting.gravatar_enabled?
1281 options.merge!(:default => Setting.gravatar_default)
1281 options.merge!(:default => Setting.gravatar_default)
1282 email = nil
1282 email = nil
1283 if user.respond_to?(:mail)
1283 if user.respond_to?(:mail)
1284 email = user.mail
1284 email = user.mail
1285 elsif user.to_s =~ %r{<(.+?)>}
1285 elsif user.to_s =~ %r{<(.+?)>}
1286 email = $1
1286 email = $1
1287 end
1287 end
1288 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1288 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1289 else
1289 else
1290 ''
1290 ''
1291 end
1291 end
1292 end
1292 end
1293
1293
1294 # Returns a link to edit user's avatar if avatars are enabled
1294 # Returns a link to edit user's avatar if avatars are enabled
1295 def avatar_edit_link(user, options={})
1295 def avatar_edit_link(user, options={})
1296 if Setting.gravatar_enabled?
1296 if Setting.gravatar_enabled?
1297 url = "https://gravatar.com"
1297 url = "https://gravatar.com"
1298 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1298 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1299 end
1299 end
1300 end
1300 end
1301
1301
1302 def sanitize_anchor_name(anchor)
1302 def sanitize_anchor_name(anchor)
1303 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1303 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1304 end
1304 end
1305
1305
1306 # Returns the javascript tags that are included in the html layout head
1306 # Returns the javascript tags that are included in the html layout head
1307 def javascript_heads
1307 def javascript_heads
1308 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1308 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1309 unless User.current.pref.warn_on_leaving_unsaved == '0'
1309 unless User.current.pref.warn_on_leaving_unsaved == '0'
1310 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1310 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1311 end
1311 end
1312 tags
1312 tags
1313 end
1313 end
1314
1314
1315 def favicon
1315 def favicon
1316 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1316 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1317 end
1317 end
1318
1318
1319 # Returns the path to the favicon
1319 # Returns the path to the favicon
1320 def favicon_path
1320 def favicon_path
1321 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1321 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1322 image_path(icon)
1322 image_path(icon)
1323 end
1323 end
1324
1324
1325 # Returns the full URL to the favicon
1325 # Returns the full URL to the favicon
1326 def favicon_url
1326 def favicon_url
1327 # TODO: use #image_url introduced in Rails4
1327 # TODO: use #image_url introduced in Rails4
1328 path = favicon_path
1328 path = favicon_path
1329 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1329 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1330 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1330 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1331 end
1331 end
1332
1332
1333 def robot_exclusion_tag
1333 def robot_exclusion_tag
1334 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1334 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1335 end
1335 end
1336
1336
1337 # Returns true if arg is expected in the API response
1337 # Returns true if arg is expected in the API response
1338 def include_in_api_response?(arg)
1338 def include_in_api_response?(arg)
1339 unless @included_in_api_response
1339 unless @included_in_api_response
1340 param = params[:include]
1340 param = params[:include]
1341 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1341 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1342 @included_in_api_response.collect!(&:strip)
1342 @included_in_api_response.collect!(&:strip)
1343 end
1343 end
1344 @included_in_api_response.include?(arg.to_s)
1344 @included_in_api_response.include?(arg.to_s)
1345 end
1345 end
1346
1346
1347 # Returns options or nil if nometa param or X-Redmine-Nometa header
1347 # Returns options or nil if nometa param or X-Redmine-Nometa header
1348 # was set in the request
1348 # was set in the request
1349 def api_meta(options)
1349 def api_meta(options)
1350 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1350 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1351 # compatibility mode for activeresource clients that raise
1351 # compatibility mode for activeresource clients that raise
1352 # an error when deserializing an array with attributes
1352 # an error when deserializing an array with attributes
1353 nil
1353 nil
1354 else
1354 else
1355 options
1355 options
1356 end
1356 end
1357 end
1357 end
1358
1358
1359 def generate_csv(&block)
1359 def generate_csv(&block)
1360 decimal_separator = l(:general_csv_decimal_separator)
1360 decimal_separator = l(:general_csv_decimal_separator)
1361 encoding = l(:general_csv_encoding)
1361 encoding = l(:general_csv_encoding)
1362 end
1362 end
1363
1363
1364 private
1364 private
1365
1365
1366 def wiki_helper
1366 def wiki_helper
1367 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1367 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1368 extend helper
1368 extend helper
1369 return self
1369 return self
1370 end
1370 end
1371 end
1371 end
@@ -1,1084 +1,1087
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 group = role.anonymous? ? Group.anonymous : Group.non_member
200 group = role.anonymous? ? Group.anonymous : Group.non_member
201 principal_ids = [user.id, group.id].compact
201 principal_ids = [user.id, group.id].compact
202 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id IN (#{principal_ids.join(',')})))"
202 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id IN (#{principal_ids.join(',')})))"
203 end
203 end
204 statement_by_role[role] = s
204 statement_by_role[role] = s
205 end
205 end
206 end
206 end
207 user.projects_by_role.each do |role, projects|
207 user.projects_by_role.each do |role, projects|
208 if role.allowed_to?(permission) && projects.any?
208 if role.allowed_to?(permission) && projects.any?
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
210 end
210 end
211 end
211 end
212 if statement_by_role.empty?
212 if statement_by_role.empty?
213 "1=0"
213 "1=0"
214 else
214 else
215 if block_given?
215 if block_given?
216 statement_by_role.each do |role, statement|
216 statement_by_role.each do |role, statement|
217 if s = yield(role, user)
217 if s = yield(role, user)
218 statement_by_role[role] = "(#{statement} AND (#{s}))"
218 statement_by_role[role] = "(#{statement} AND (#{s}))"
219 end
219 end
220 end
220 end
221 end
221 end
222 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
222 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
223 end
223 end
224 end
224 end
225 end
225 end
226
226
227 def override_roles(role)
227 def override_roles(role)
228 @override_members ||= memberships.
228 @override_members ||= memberships.
229 joins(:principal).
229 joins(:principal).
230 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
230 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
231
231
232 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
232 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
233 member = @override_members.detect {|m| m.principal.is_a? group_class}
233 member = @override_members.detect {|m| m.principal.is_a? group_class}
234 member ? member.roles.to_a : [role]
234 member ? member.roles.to_a : [role]
235 end
235 end
236
236
237 def principals
237 def principals
238 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
238 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
239 end
239 end
240
240
241 def users
241 def users
242 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
242 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
243 end
243 end
244
244
245 # Returns the Systemwide and project specific activities
245 # Returns the Systemwide and project specific activities
246 def activities(include_inactive=false)
246 def activities(include_inactive=false)
247 t = TimeEntryActivity.table_name
247 t = TimeEntryActivity.table_name
248 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
248 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
249
249
250 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
250 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
251 if overridden_activity_ids.any?
251 if overridden_activity_ids.any?
252 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
252 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
253 end
253 end
254 unless include_inactive
254 unless include_inactive
255 scope = scope.active
255 scope = scope.active
256 end
256 end
257 scope
257 scope
258 end
258 end
259
259
260 # Will create a new Project specific Activity or update an existing one
260 # Will create a new Project specific Activity or update an existing one
261 #
261 #
262 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
262 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
263 # does not successfully save.
263 # does not successfully save.
264 def update_or_create_time_entry_activity(id, activity_hash)
264 def update_or_create_time_entry_activity(id, activity_hash)
265 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
265 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
266 self.create_time_entry_activity_if_needed(activity_hash)
266 self.create_time_entry_activity_if_needed(activity_hash)
267 else
267 else
268 activity = project.time_entry_activities.find_by_id(id.to_i)
268 activity = project.time_entry_activities.find_by_id(id.to_i)
269 activity.update_attributes(activity_hash) if activity
269 activity.update_attributes(activity_hash) if activity
270 end
270 end
271 end
271 end
272
272
273 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
273 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
274 #
274 #
275 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
275 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
276 # does not successfully save.
276 # does not successfully save.
277 def create_time_entry_activity_if_needed(activity)
277 def create_time_entry_activity_if_needed(activity)
278 if activity['parent_id']
278 if activity['parent_id']
279 parent_activity = TimeEntryActivity.find(activity['parent_id'])
279 parent_activity = TimeEntryActivity.find(activity['parent_id'])
280 activity['name'] = parent_activity.name
280 activity['name'] = parent_activity.name
281 activity['position'] = parent_activity.position
281 activity['position'] = parent_activity.position
282 if Enumeration.overriding_change?(activity, parent_activity)
282 if Enumeration.overriding_change?(activity, parent_activity)
283 project_activity = self.time_entry_activities.create(activity)
283 project_activity = self.time_entry_activities.create(activity)
284 if project_activity.new_record?
284 if project_activity.new_record?
285 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
285 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
286 else
286 else
287 self.time_entries.
287 self.time_entries.
288 where(:activity_id => parent_activity.id).
288 where(:activity_id => parent_activity.id).
289 update_all(:activity_id => project_activity.id)
289 update_all(:activity_id => project_activity.id)
290 end
290 end
291 end
291 end
292 end
292 end
293 end
293 end
294
294
295 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
295 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
296 #
296 #
297 # Examples:
297 # Examples:
298 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
298 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
299 # project.project_condition(false) => "projects.id = 1"
299 # project.project_condition(false) => "projects.id = 1"
300 def project_condition(with_subprojects)
300 def project_condition(with_subprojects)
301 cond = "#{Project.table_name}.id = #{id}"
301 cond = "#{Project.table_name}.id = #{id}"
302 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
302 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
303 cond
303 cond
304 end
304 end
305
305
306 def self.find(*args)
306 def self.find(*args)
307 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
307 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
308 project = find_by_identifier(*args)
308 project = find_by_identifier(*args)
309 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
309 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
310 project
310 project
311 else
311 else
312 super
312 super
313 end
313 end
314 end
314 end
315
315
316 def self.find_by_param(*args)
316 def self.find_by_param(*args)
317 self.find(*args)
317 self.find(*args)
318 end
318 end
319
319
320 alias :base_reload :reload
320 alias :base_reload :reload
321 def reload(*args)
321 def reload(*args)
322 @principals = nil
322 @principals = nil
323 @users = nil
323 @users = nil
324 @shared_versions = nil
324 @shared_versions = nil
325 @rolled_up_versions = nil
325 @rolled_up_versions = nil
326 @rolled_up_trackers = nil
326 @rolled_up_trackers = nil
327 @all_issue_custom_fields = nil
327 @all_issue_custom_fields = nil
328 @all_time_entry_custom_fields = nil
328 @all_time_entry_custom_fields = nil
329 @to_param = nil
329 @to_param = nil
330 @allowed_parents = nil
330 @allowed_parents = nil
331 @allowed_permissions = nil
331 @allowed_permissions = nil
332 @actions_allowed = nil
332 @actions_allowed = nil
333 @start_date = nil
333 @start_date = nil
334 @due_date = nil
334 @due_date = nil
335 @override_members = nil
335 @override_members = nil
336 @assignable_users = nil
336 @assignable_users = nil
337 base_reload(*args)
337 base_reload(*args)
338 end
338 end
339
339
340 def to_param
340 def to_param
341 if new_record?
341 if new_record?
342 nil
342 nil
343 else
343 else
344 # id is used for projects with a numeric identifier (compatibility)
344 # id is used for projects with a numeric identifier (compatibility)
345 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
345 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
346 end
346 end
347 end
347 end
348
348
349 def active?
349 def active?
350 self.status == STATUS_ACTIVE
350 self.status == STATUS_ACTIVE
351 end
351 end
352
352
353 def archived?
353 def archived?
354 self.status == STATUS_ARCHIVED
354 self.status == STATUS_ARCHIVED
355 end
355 end
356
356
357 # Archives the project and its descendants
357 # Archives the project and its descendants
358 def archive
358 def archive
359 # Check that there is no issue of a non descendant project that is assigned
359 # Check that there is no issue of a non descendant project that is assigned
360 # to one of the project or descendant versions
360 # to one of the project or descendant versions
361 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
361 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
362
362
363 if version_ids.any? &&
363 if version_ids.any? &&
364 Issue.
364 Issue.
365 includes(:project).
365 includes(:project).
366 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
366 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
367 where(:fixed_version_id => version_ids).
367 where(:fixed_version_id => version_ids).
368 exists?
368 exists?
369 return false
369 return false
370 end
370 end
371 Project.transaction do
371 Project.transaction do
372 archive!
372 archive!
373 end
373 end
374 true
374 true
375 end
375 end
376
376
377 # Unarchives the project
377 # Unarchives the project
378 # All its ancestors must be active
378 # All its ancestors must be active
379 def unarchive
379 def unarchive
380 return false if ancestors.detect {|a| !a.active?}
380 return false if ancestors.detect {|a| !a.active?}
381 update_attribute :status, STATUS_ACTIVE
381 update_attribute :status, STATUS_ACTIVE
382 end
382 end
383
383
384 def close
384 def close
385 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
385 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
386 end
386 end
387
387
388 def reopen
388 def reopen
389 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
389 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
390 end
390 end
391
391
392 # Returns an array of projects the project can be moved to
392 # Returns an array of projects the project can be moved to
393 # by the current user
393 # by the current user
394 def allowed_parents(user=User.current)
394 def allowed_parents(user=User.current)
395 return @allowed_parents if @allowed_parents
395 return @allowed_parents if @allowed_parents
396 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
396 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
397 @allowed_parents = @allowed_parents - self_and_descendants
397 @allowed_parents = @allowed_parents - self_and_descendants
398 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
398 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
399 @allowed_parents << nil
399 @allowed_parents << nil
400 end
400 end
401 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
401 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
402 @allowed_parents << parent
402 @allowed_parents << parent
403 end
403 end
404 @allowed_parents
404 @allowed_parents
405 end
405 end
406
406
407 # Sets the parent of the project with authorization check
407 # Sets the parent of the project with authorization check
408 def set_allowed_parent!(p)
408 def set_allowed_parent!(p)
409 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
409 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
410 p = p.id if p.is_a?(Project)
410 p = p.id if p.is_a?(Project)
411 send :safe_attributes, {:project_id => p}
411 send :safe_attributes, {:project_id => p}
412 save
412 save
413 end
413 end
414
414
415 # Sets the parent of the project and saves the project
415 # Sets the parent of the project and saves the project
416 # Argument can be either a Project, a String, a Fixnum or nil
416 # Argument can be either a Project, a String, a Fixnum or nil
417 def set_parent!(p)
417 def set_parent!(p)
418 if p.is_a?(Project)
418 if p.is_a?(Project)
419 self.parent = p
419 self.parent = p
420 else
420 else
421 self.parent_id = p
421 self.parent_id = p
422 end
422 end
423 save
423 save
424 end
424 end
425
425
426 # Returns a scope of the trackers used by the project and its active sub projects
426 # Returns a scope of the trackers used by the project and its active sub projects
427 def rolled_up_trackers(include_subprojects=true)
427 def rolled_up_trackers(include_subprojects=true)
428 if include_subprojects
428 if include_subprojects
429 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
429 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
431 else
431 else
432 rolled_up_trackers_base_scope.
432 rolled_up_trackers_base_scope.
433 where(:projects => {:id => id})
433 where(:projects => {:id => id})
434 end
434 end
435 end
435 end
436
436
437 def rolled_up_trackers_base_scope
437 def rolled_up_trackers_base_scope
438 Tracker.
438 Tracker.
439 joins(projects: :enabled_modules).
439 joins(projects: :enabled_modules).
440 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
440 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
441 where(:enabled_modules => {:name => 'issue_tracking'}).
441 where(:enabled_modules => {:name => 'issue_tracking'}).
442 distinct.
442 distinct.
443 sorted
443 sorted
444 end
444 end
445
445
446 # Closes open and locked project versions that are completed
446 # Closes open and locked project versions that are completed
447 def close_completed_versions
447 def close_completed_versions
448 Version.transaction do
448 Version.transaction do
449 versions.where(:status => %w(open locked)).each do |version|
449 versions.where(:status => %w(open locked)).each do |version|
450 if version.completed?
450 if version.completed?
451 version.update_attribute(:status, 'closed')
451 version.update_attribute(:status, 'closed')
452 end
452 end
453 end
453 end
454 end
454 end
455 end
455 end
456
456
457 # Returns a scope of the Versions on subprojects
457 # Returns a scope of the Versions on subprojects
458 def rolled_up_versions
458 def rolled_up_versions
459 @rolled_up_versions ||=
459 @rolled_up_versions ||=
460 Version.
460 Version.
461 joins(:project).
461 joins(:project).
462 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
462 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
463 end
463 end
464
464
465 # Returns a scope of the Versions used by the project
465 # Returns a scope of the Versions used by the project
466 def shared_versions
466 def shared_versions
467 if new_record?
467 if new_record?
468 Version.
468 Version.
469 joins(:project).
469 joins(:project).
470 preload(:project).
470 preload(:project).
471 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
471 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
472 else
472 else
473 @shared_versions ||= begin
473 @shared_versions ||= begin
474 r = root? ? self : root
474 r = root? ? self : root
475 Version.
475 Version.
476 joins(:project).
476 joins(:project).
477 preload(:project).
477 preload(:project).
478 where("#{Project.table_name}.id = #{id}" +
478 where("#{Project.table_name}.id = #{id}" +
479 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
479 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
480 " #{Version.table_name}.sharing = 'system'" +
480 " #{Version.table_name}.sharing = 'system'" +
481 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
481 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
482 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
482 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
483 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
483 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
484 "))")
484 "))")
485 end
485 end
486 end
486 end
487 end
487 end
488
488
489 # Returns a hash of project users grouped by role
489 # Returns a hash of project users grouped by role
490 def users_by_role
490 def users_by_role
491 members.includes(:user, :roles).inject({}) do |h, m|
491 members.includes(:user, :roles).inject({}) do |h, m|
492 m.roles.each do |r|
492 m.roles.each do |r|
493 h[r] ||= []
493 h[r] ||= []
494 h[r] << m.user
494 h[r] << m.user
495 end
495 end
496 h
496 h
497 end
497 end
498 end
498 end
499
499
500 # Adds user as a project member with the default role
500 # Adds user as a project member with the default role
501 # Used for when a non-admin user creates a project
501 # Used for when a non-admin user creates a project
502 def add_default_member(user)
502 def add_default_member(user)
503 role = self.class.default_member_role
503 role = self.class.default_member_role
504 member = Member.new(:project => self, :principal => user, :roles => [role])
504 member = Member.new(:project => self, :principal => user, :roles => [role])
505 self.members << member
505 self.members << member
506 member
506 member
507 end
507 end
508
508
509 # Default role that is given to non-admin users that
509 # Default role that is given to non-admin users that
510 # create a project
510 # create a project
511 def self.default_member_role
511 def self.default_member_role
512 Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
512 Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
513 end
513 end
514
514
515 # Deletes all project's members
515 # Deletes all project's members
516 def delete_all_members
516 def delete_all_members
517 me, mr = Member.table_name, MemberRole.table_name
517 me, mr = Member.table_name, MemberRole.table_name
518 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
518 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
519 Member.where(:project_id => id).delete_all
519 Member.where(:project_id => id).delete_all
520 end
520 end
521
521
522 # Return a Principal scope of users/groups issues can be assigned to
522 # Return a Principal scope of users/groups issues can be assigned to
523 def assignable_users(tracker=nil)
523 def assignable_users(tracker=nil)
524 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
524 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
525
525
526 types = ['User']
526 types = ['User']
527 types << 'Group' if Setting.issue_group_assignment?
527 types << 'Group' if Setting.issue_group_assignment?
528
528
529 scope = Principal.
529 scope = Principal.
530 active.
530 active.
531 joins(:members => :roles).
531 joins(:members => :roles).
532 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
532 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
533 distinct.
533 distinct.
534 sorted
534 sorted
535
535
536 if tracker
536 if tracker
537 # Rejects users that cannot the view the tracker
537 # Rejects users that cannot the view the tracker
538 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
538 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
539 scope = scope.where(:roles => {:id => roles.map(&:id)})
539 scope = scope.where(:roles => {:id => roles.map(&:id)})
540 end
540 end
541
541
542 @assignable_users ||= {}
542 @assignable_users ||= {}
543 @assignable_users[tracker] = scope
543 @assignable_users[tracker] = scope
544 end
544 end
545
545
546 # Returns the mail addresses of users that should be always notified on project events
546 # Returns the mail addresses of users that should be always notified on project events
547 def recipients
547 def recipients
548 notified_users.collect {|user| user.mail}
548 notified_users.collect {|user| user.mail}
549 end
549 end
550
550
551 # Returns the users that should be notified on project events
551 # Returns the users that should be notified on project events
552 def notified_users
552 def notified_users
553 # TODO: User part should be extracted to User#notify_about?
553 # TODO: User part should be extracted to User#notify_about?
554 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
554 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
555 end
555 end
556
556
557 # Returns a scope of all custom fields enabled for project issues
557 # Returns a scope of all custom fields enabled for project issues
558 # (explicitly associated custom fields and custom fields enabled for all projects)
558 # (explicitly associated custom fields and custom fields enabled for all projects)
559 def all_issue_custom_fields
559 def all_issue_custom_fields
560 if new_record?
560 if new_record?
561 @all_issue_custom_fields ||= IssueCustomField.
561 @all_issue_custom_fields ||= IssueCustomField.
562 sorted.
562 sorted.
563 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
563 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
564 else
564 else
565 @all_issue_custom_fields ||= IssueCustomField.
565 @all_issue_custom_fields ||= IssueCustomField.
566 sorted.
566 sorted.
567 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
567 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
568 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
568 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
569 " WHERE cfp.project_id = ?)", true, id)
569 " WHERE cfp.project_id = ?)", true, id)
570 end
570 end
571 end
571 end
572
572
573 def project
573 def project
574 self
574 self
575 end
575 end
576
576
577 def <=>(project)
577 def <=>(project)
578 name.casecmp(project.name)
578 name.casecmp(project.name)
579 end
579 end
580
580
581 def to_s
581 def to_s
582 name
582 name
583 end
583 end
584
584
585 # Returns a short description of the projects (first lines)
585 # Returns a short description of the projects (first lines)
586 def short_description(length = 255)
586 def short_description(length = 255)
587 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
587 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
588 end
588 end
589
589
590 def css_classes
590 def css_classes
591 s = 'project'
591 s = 'project'
592 s << ' root' if root?
592 s << ' root' if root?
593 s << ' child' if child?
593 s << ' child' if child?
594 s << (leaf? ? ' leaf' : ' parent')
594 s << (leaf? ? ' leaf' : ' parent')
595 unless active?
595 unless active?
596 if archived?
596 if archived?
597 s << ' archived'
597 s << ' archived'
598 else
598 else
599 s << ' closed'
599 s << ' closed'
600 end
600 end
601 end
601 end
602 s
602 s
603 end
603 end
604
604
605 # The earliest start date of a project, based on it's issues and versions
605 # The earliest start date of a project, based on it's issues and versions
606 def start_date
606 def start_date
607 @start_date ||= [
607 @start_date ||= [
608 issues.minimum('start_date'),
608 issues.minimum('start_date'),
609 shared_versions.minimum('effective_date'),
609 shared_versions.minimum('effective_date'),
610 Issue.fixed_version(shared_versions).minimum('start_date')
610 Issue.fixed_version(shared_versions).minimum('start_date')
611 ].compact.min
611 ].compact.min
612 end
612 end
613
613
614 # The latest due date of an issue or version
614 # The latest due date of an issue or version
615 def due_date
615 def due_date
616 @due_date ||= [
616 @due_date ||= [
617 issues.maximum('due_date'),
617 issues.maximum('due_date'),
618 shared_versions.maximum('effective_date'),
618 shared_versions.maximum('effective_date'),
619 Issue.fixed_version(shared_versions).maximum('due_date')
619 Issue.fixed_version(shared_versions).maximum('due_date')
620 ].compact.max
620 ].compact.max
621 end
621 end
622
622
623 def overdue?
623 def overdue?
624 active? && !due_date.nil? && (due_date < User.current.today)
624 active? && !due_date.nil? && (due_date < User.current.today)
625 end
625 end
626
626
627 # Returns the percent completed for this project, based on the
627 # Returns the percent completed for this project, based on the
628 # progress on it's versions.
628 # progress on it's versions.
629 def completed_percent(options={:include_subprojects => false})
629 def completed_percent(options={:include_subprojects => false})
630 if options.delete(:include_subprojects)
630 if options.delete(:include_subprojects)
631 total = self_and_descendants.collect(&:completed_percent).sum
631 total = self_and_descendants.collect(&:completed_percent).sum
632
632
633 total / self_and_descendants.count
633 total / self_and_descendants.count
634 else
634 else
635 if versions.count > 0
635 if versions.count > 0
636 total = versions.collect(&:completed_percent).sum
636 total = versions.collect(&:completed_percent).sum
637
637
638 total / versions.count
638 total / versions.count
639 else
639 else
640 100
640 100
641 end
641 end
642 end
642 end
643 end
643 end
644
644
645 # Return true if this project allows to do the specified action.
645 # Return true if this project allows to do the specified action.
646 # action can be:
646 # action can be:
647 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
647 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
648 # * a permission Symbol (eg. :edit_project)
648 # * a permission Symbol (eg. :edit_project)
649 def allows_to?(action)
649 def allows_to?(action)
650 if archived?
650 if archived?
651 # No action allowed on archived projects
651 # No action allowed on archived projects
652 return false
652 return false
653 end
653 end
654 unless active? || Redmine::AccessControl.read_action?(action)
654 unless active? || Redmine::AccessControl.read_action?(action)
655 # No write action allowed on closed projects
655 # No write action allowed on closed projects
656 return false
656 return false
657 end
657 end
658 # No action allowed on disabled modules
658 # No action allowed on disabled modules
659 if action.is_a? Hash
659 if action.is_a? Hash
660 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
660 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
661 else
661 else
662 allowed_permissions.include? action
662 allowed_permissions.include? action
663 end
663 end
664 end
664 end
665
665
666 # Return the enabled module with the given name
666 # Return the enabled module with the given name
667 # or nil if the module is not enabled for the project
667 # or nil if the module is not enabled for the project
668 def enabled_module(name)
668 def enabled_module(name)
669 name = name.to_s
669 name = name.to_s
670 enabled_modules.detect {|m| m.name == name}
670 enabled_modules.detect {|m| m.name == name}
671 end
671 end
672
672
673 # Return true if the module with the given name is enabled
673 # Return true if the module with the given name is enabled
674 def module_enabled?(name)
674 def module_enabled?(name)
675 enabled_module(name).present?
675 enabled_module(name).present?
676 end
676 end
677
677
678 def enabled_module_names=(module_names)
678 def enabled_module_names=(module_names)
679 if module_names && module_names.is_a?(Array)
679 if module_names && module_names.is_a?(Array)
680 module_names = module_names.collect(&:to_s).reject(&:blank?)
680 module_names = module_names.collect(&:to_s).reject(&:blank?)
681 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
681 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
682 else
682 else
683 enabled_modules.clear
683 enabled_modules.clear
684 end
684 end
685 end
685 end
686
686
687 # Returns an array of the enabled modules names
687 # Returns an array of the enabled modules names
688 def enabled_module_names
688 def enabled_module_names
689 enabled_modules.collect(&:name)
689 enabled_modules.collect(&:name)
690 end
690 end
691
691
692 # Enable a specific module
692 # Enable a specific module
693 #
693 #
694 # Examples:
694 # Examples:
695 # project.enable_module!(:issue_tracking)
695 # project.enable_module!(:issue_tracking)
696 # project.enable_module!("issue_tracking")
696 # project.enable_module!("issue_tracking")
697 def enable_module!(name)
697 def enable_module!(name)
698 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
698 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
699 end
699 end
700
700
701 # Disable a module if it exists
701 # Disable a module if it exists
702 #
702 #
703 # Examples:
703 # Examples:
704 # project.disable_module!(:issue_tracking)
704 # project.disable_module!(:issue_tracking)
705 # project.disable_module!("issue_tracking")
705 # project.disable_module!("issue_tracking")
706 # project.disable_module!(project.enabled_modules.first)
706 # project.disable_module!(project.enabled_modules.first)
707 def disable_module!(target)
707 def disable_module!(target)
708 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
708 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
709 target.destroy unless target.blank?
709 target.destroy unless target.blank?
710 end
710 end
711
711
712 safe_attributes 'name',
712 safe_attributes 'name',
713 'description',
713 'description',
714 'homepage',
714 'homepage',
715 'is_public',
715 'is_public',
716 'identifier',
716 'identifier',
717 'custom_field_values',
717 'custom_field_values',
718 'custom_fields',
718 'custom_fields',
719 'tracker_ids',
719 'tracker_ids',
720 'issue_custom_field_ids',
720 'issue_custom_field_ids',
721 'parent_id',
721 'parent_id',
722 'default_version_id'
722 'default_version_id'
723
723
724 safe_attributes 'enabled_module_names',
724 safe_attributes 'enabled_module_names',
725 :if => lambda {|project, user|
725 :if => lambda {|project, user|
726 if project.new_record?
726 if project.new_record?
727 if user.admin?
727 if user.admin?
728 true
728 true
729 else
729 else
730 default_member_role.has_permission?(:select_project_modules)
730 default_member_role.has_permission?(:select_project_modules)
731 end
731 end
732 else
732 else
733 user.allowed_to?(:select_project_modules, project)
733 user.allowed_to?(:select_project_modules, project)
734 end
734 end
735 }
735 }
736
736
737 safe_attributes 'inherit_members',
737 safe_attributes 'inherit_members',
738 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
738 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
739
739
740 def safe_attributes=(attrs, user=User.current)
740 def safe_attributes=(attrs, user=User.current)
741 return unless attrs.is_a?(Hash)
741 return unless attrs.is_a?(Hash)
742 attrs = attrs.deep_dup
742 attrs = attrs.deep_dup
743
743
744 @unallowed_parent_id = nil
744 @unallowed_parent_id = nil
745 if new_record? || attrs.key?('parent_id')
745 if new_record? || attrs.key?('parent_id')
746 parent_id_param = attrs['parent_id'].to_s
746 parent_id_param = attrs['parent_id'].to_s
747 if new_record? || parent_id_param != parent_id.to_s
747 if new_record? || parent_id_param != parent_id.to_s
748 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
748 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
749 unless allowed_parents(user).include?(p)
749 unless allowed_parents(user).include?(p)
750 attrs.delete('parent_id')
750 attrs.delete('parent_id')
751 @unallowed_parent_id = true
751 @unallowed_parent_id = true
752 end
752 end
753 end
753 end
754 end
754 end
755
755
756 super(attrs, user)
756 super(attrs, user)
757 end
757 end
758
758
759 # Returns an auto-generated project identifier based on the last identifier used
759 # Returns an auto-generated project identifier based on the last identifier used
760 def self.next_identifier
760 def self.next_identifier
761 p = Project.order('id DESC').first
761 p = Project.order('id DESC').first
762 p.nil? ? nil : p.identifier.to_s.succ
762 p.nil? ? nil : p.identifier.to_s.succ
763 end
763 end
764
764
765 # Copies and saves the Project instance based on the +project+.
765 # Copies and saves the Project instance based on the +project+.
766 # Duplicates the source project's:
766 # Duplicates the source project's:
767 # * Wiki
767 # * Wiki
768 # * Versions
768 # * Versions
769 # * Categories
769 # * Categories
770 # * Issues
770 # * Issues
771 # * Members
771 # * Members
772 # * Queries
772 # * Queries
773 #
773 #
774 # Accepts an +options+ argument to specify what to copy
774 # Accepts an +options+ argument to specify what to copy
775 #
775 #
776 # Examples:
776 # Examples:
777 # project.copy(1) # => copies everything
777 # project.copy(1) # => copies everything
778 # project.copy(1, :only => 'members') # => copies members only
778 # project.copy(1, :only => 'members') # => copies members only
779 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
779 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
780 def copy(project, options={})
780 def copy(project, options={})
781 project = project.is_a?(Project) ? project : Project.find(project)
781 project = project.is_a?(Project) ? project : Project.find(project)
782
782
783 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
783 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
784 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
784 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
785
785
786 Project.transaction do
786 Project.transaction do
787 if save
787 if save
788 reload
788 reload
789 to_be_copied.each do |name|
789 to_be_copied.each do |name|
790 send "copy_#{name}", project
790 send "copy_#{name}", project
791 end
791 end
792 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
792 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
793 save
793 save
794 else
794 else
795 false
795 false
796 end
796 end
797 end
797 end
798 end
798 end
799
799
800 def member_principals
800 def member_principals
801 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
801 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
802 memberships.active
802 memberships.active
803 end
803 end
804
804
805 # Returns a new unsaved Project instance with attributes copied from +project+
805 # Returns a new unsaved Project instance with attributes copied from +project+
806 def self.copy_from(project)
806 def self.copy_from(project)
807 project = project.is_a?(Project) ? project : Project.find(project)
807 project = project.is_a?(Project) ? project : Project.find(project)
808 # clear unique attributes
808 # clear unique attributes
809 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
809 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
810 copy = Project.new(attributes)
810 copy = Project.new(attributes)
811 copy.enabled_module_names = project.enabled_module_names
811 copy.enabled_module_names = project.enabled_module_names
812 copy.trackers = project.trackers
812 copy.trackers = project.trackers
813 copy.custom_values = project.custom_values.collect {|v| v.clone}
813 copy.custom_values = project.custom_values.collect {|v| v.clone}
814 copy.issue_custom_fields = project.issue_custom_fields
814 copy.issue_custom_fields = project.issue_custom_fields
815 copy
815 copy
816 end
816 end
817
817
818 # Yields the given block for each project with its level in the tree
818 # Yields the given block for each project with its level in the tree
819 def self.project_tree(projects, &block)
819 def self.project_tree(projects, options={}, &block)
820 ancestors = []
820 ancestors = []
821 if options[:init_level] && projects.first
822 ancestors = projects.first.ancestors.to_a
823 end
821 projects.sort_by(&:lft).each do |project|
824 projects.sort_by(&:lft).each do |project|
822 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
825 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
823 ancestors.pop
826 ancestors.pop
824 end
827 end
825 yield project, ancestors.size
828 yield project, ancestors.size
826 ancestors << project
829 ancestors << project
827 end
830 end
828 end
831 end
829
832
830 private
833 private
831
834
832 def update_inherited_members
835 def update_inherited_members
833 if parent
836 if parent
834 if inherit_members? && !inherit_members_was
837 if inherit_members? && !inherit_members_was
835 remove_inherited_member_roles
838 remove_inherited_member_roles
836 add_inherited_member_roles
839 add_inherited_member_roles
837 elsif !inherit_members? && inherit_members_was
840 elsif !inherit_members? && inherit_members_was
838 remove_inherited_member_roles
841 remove_inherited_member_roles
839 end
842 end
840 end
843 end
841 end
844 end
842
845
843 def remove_inherited_member_roles
846 def remove_inherited_member_roles
844 member_roles = memberships.map(&:member_roles).flatten
847 member_roles = memberships.map(&:member_roles).flatten
845 member_role_ids = member_roles.map(&:id)
848 member_role_ids = member_roles.map(&:id)
846 member_roles.each do |member_role|
849 member_roles.each do |member_role|
847 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
850 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
848 member_role.destroy
851 member_role.destroy
849 end
852 end
850 end
853 end
851 end
854 end
852
855
853 def add_inherited_member_roles
856 def add_inherited_member_roles
854 if inherit_members? && parent
857 if inherit_members? && parent
855 parent.memberships.each do |parent_member|
858 parent.memberships.each do |parent_member|
856 member = Member.find_or_new(self.id, parent_member.user_id)
859 member = Member.find_or_new(self.id, parent_member.user_id)
857 parent_member.member_roles.each do |parent_member_role|
860 parent_member.member_roles.each do |parent_member_role|
858 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
861 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
859 end
862 end
860 member.save!
863 member.save!
861 end
864 end
862 memberships.reset
865 memberships.reset
863 end
866 end
864 end
867 end
865
868
866 def update_versions_from_hierarchy_change
869 def update_versions_from_hierarchy_change
867 Issue.update_versions_from_hierarchy_change(self)
870 Issue.update_versions_from_hierarchy_change(self)
868 end
871 end
869
872
870 def validate_parent
873 def validate_parent
871 if @unallowed_parent_id
874 if @unallowed_parent_id
872 errors.add(:parent_id, :invalid)
875 errors.add(:parent_id, :invalid)
873 elsif parent_id_changed?
876 elsif parent_id_changed?
874 unless parent.nil? || (parent.active? && move_possible?(parent))
877 unless parent.nil? || (parent.active? && move_possible?(parent))
875 errors.add(:parent_id, :invalid)
878 errors.add(:parent_id, :invalid)
876 end
879 end
877 end
880 end
878 end
881 end
879
882
880 # Copies wiki from +project+
883 # Copies wiki from +project+
881 def copy_wiki(project)
884 def copy_wiki(project)
882 # Check that the source project has a wiki first
885 # Check that the source project has a wiki first
883 unless project.wiki.nil?
886 unless project.wiki.nil?
884 wiki = self.wiki || Wiki.new
887 wiki = self.wiki || Wiki.new
885 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
888 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
886 wiki_pages_map = {}
889 wiki_pages_map = {}
887 project.wiki.pages.each do |page|
890 project.wiki.pages.each do |page|
888 # Skip pages without content
891 # Skip pages without content
889 next if page.content.nil?
892 next if page.content.nil?
890 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
893 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
891 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
894 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
892 new_wiki_page.content = new_wiki_content
895 new_wiki_page.content = new_wiki_content
893 wiki.pages << new_wiki_page
896 wiki.pages << new_wiki_page
894 wiki_pages_map[page.id] = new_wiki_page
897 wiki_pages_map[page.id] = new_wiki_page
895 end
898 end
896
899
897 self.wiki = wiki
900 self.wiki = wiki
898 wiki.save
901 wiki.save
899 # Reproduce page hierarchy
902 # Reproduce page hierarchy
900 project.wiki.pages.each do |page|
903 project.wiki.pages.each do |page|
901 if page.parent_id && wiki_pages_map[page.id]
904 if page.parent_id && wiki_pages_map[page.id]
902 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
905 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
903 wiki_pages_map[page.id].save
906 wiki_pages_map[page.id].save
904 end
907 end
905 end
908 end
906 end
909 end
907 end
910 end
908
911
909 # Copies versions from +project+
912 # Copies versions from +project+
910 def copy_versions(project)
913 def copy_versions(project)
911 project.versions.each do |version|
914 project.versions.each do |version|
912 new_version = Version.new
915 new_version = Version.new
913 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
916 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
914 self.versions << new_version
917 self.versions << new_version
915 end
918 end
916 end
919 end
917
920
918 # Copies issue categories from +project+
921 # Copies issue categories from +project+
919 def copy_issue_categories(project)
922 def copy_issue_categories(project)
920 project.issue_categories.each do |issue_category|
923 project.issue_categories.each do |issue_category|
921 new_issue_category = IssueCategory.new
924 new_issue_category = IssueCategory.new
922 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
925 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
923 self.issue_categories << new_issue_category
926 self.issue_categories << new_issue_category
924 end
927 end
925 end
928 end
926
929
927 # Copies issues from +project+
930 # Copies issues from +project+
928 def copy_issues(project)
931 def copy_issues(project)
929 # Stores the source issue id as a key and the copied issues as the
932 # Stores the source issue id as a key and the copied issues as the
930 # value. Used to map the two together for issue relations.
933 # value. Used to map the two together for issue relations.
931 issues_map = {}
934 issues_map = {}
932
935
933 # Store status and reopen locked/closed versions
936 # Store status and reopen locked/closed versions
934 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
937 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
935 version_statuses.each do |version, status|
938 version_statuses.each do |version, status|
936 version.update_attribute :status, 'open'
939 version.update_attribute :status, 'open'
937 end
940 end
938
941
939 # Get issues sorted by root_id, lft so that parent issues
942 # Get issues sorted by root_id, lft so that parent issues
940 # get copied before their children
943 # get copied before their children
941 project.issues.reorder('root_id, lft').each do |issue|
944 project.issues.reorder('root_id, lft').each do |issue|
942 new_issue = Issue.new
945 new_issue = Issue.new
943 new_issue.copy_from(issue, :subtasks => false, :link => false)
946 new_issue.copy_from(issue, :subtasks => false, :link => false)
944 new_issue.project = self
947 new_issue.project = self
945 # Changing project resets the custom field values
948 # Changing project resets the custom field values
946 # TODO: handle this in Issue#project=
949 # TODO: handle this in Issue#project=
947 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
950 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
948 # Reassign fixed_versions by name, since names are unique per project
951 # Reassign fixed_versions by name, since names are unique per project
949 if issue.fixed_version && issue.fixed_version.project == project
952 if issue.fixed_version && issue.fixed_version.project == project
950 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
953 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
951 end
954 end
952 # Reassign version custom field values
955 # Reassign version custom field values
953 new_issue.custom_field_values.each do |custom_value|
956 new_issue.custom_field_values.each do |custom_value|
954 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
957 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
955 versions = Version.where(:id => custom_value.value).to_a
958 versions = Version.where(:id => custom_value.value).to_a
956 new_value = versions.map do |version|
959 new_value = versions.map do |version|
957 if version.project == project
960 if version.project == project
958 self.versions.detect {|v| v.name == version.name}.try(:id)
961 self.versions.detect {|v| v.name == version.name}.try(:id)
959 else
962 else
960 version.id
963 version.id
961 end
964 end
962 end
965 end
963 new_value.compact!
966 new_value.compact!
964 new_value = new_value.first unless custom_value.custom_field.multiple?
967 new_value = new_value.first unless custom_value.custom_field.multiple?
965 custom_value.value = new_value
968 custom_value.value = new_value
966 end
969 end
967 end
970 end
968 # Reassign the category by name, since names are unique per project
971 # Reassign the category by name, since names are unique per project
969 if issue.category
972 if issue.category
970 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
973 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
971 end
974 end
972 # Parent issue
975 # Parent issue
973 if issue.parent_id
976 if issue.parent_id
974 if copied_parent = issues_map[issue.parent_id]
977 if copied_parent = issues_map[issue.parent_id]
975 new_issue.parent_issue_id = copied_parent.id
978 new_issue.parent_issue_id = copied_parent.id
976 end
979 end
977 end
980 end
978
981
979 self.issues << new_issue
982 self.issues << new_issue
980 if new_issue.new_record?
983 if new_issue.new_record?
981 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
984 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
982 else
985 else
983 issues_map[issue.id] = new_issue unless new_issue.new_record?
986 issues_map[issue.id] = new_issue unless new_issue.new_record?
984 end
987 end
985 end
988 end
986
989
987 # Restore locked/closed version statuses
990 # Restore locked/closed version statuses
988 version_statuses.each do |version, status|
991 version_statuses.each do |version, status|
989 version.update_attribute :status, status
992 version.update_attribute :status, status
990 end
993 end
991
994
992 # Relations after in case issues related each other
995 # Relations after in case issues related each other
993 project.issues.each do |issue|
996 project.issues.each do |issue|
994 new_issue = issues_map[issue.id]
997 new_issue = issues_map[issue.id]
995 unless new_issue
998 unless new_issue
996 # Issue was not copied
999 # Issue was not copied
997 next
1000 next
998 end
1001 end
999
1002
1000 # Relations
1003 # Relations
1001 issue.relations_from.each do |source_relation|
1004 issue.relations_from.each do |source_relation|
1002 new_issue_relation = IssueRelation.new
1005 new_issue_relation = IssueRelation.new
1003 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1006 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1004 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
1007 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
1005 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
1008 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
1006 new_issue_relation.issue_to = source_relation.issue_to
1009 new_issue_relation.issue_to = source_relation.issue_to
1007 end
1010 end
1008 new_issue.relations_from << new_issue_relation
1011 new_issue.relations_from << new_issue_relation
1009 end
1012 end
1010
1013
1011 issue.relations_to.each do |source_relation|
1014 issue.relations_to.each do |source_relation|
1012 new_issue_relation = IssueRelation.new
1015 new_issue_relation = IssueRelation.new
1013 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1016 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1014 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
1017 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
1015 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
1018 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
1016 new_issue_relation.issue_from = source_relation.issue_from
1019 new_issue_relation.issue_from = source_relation.issue_from
1017 end
1020 end
1018 new_issue.relations_to << new_issue_relation
1021 new_issue.relations_to << new_issue_relation
1019 end
1022 end
1020 end
1023 end
1021 end
1024 end
1022
1025
1023 # Copies members from +project+
1026 # Copies members from +project+
1024 def copy_members(project)
1027 def copy_members(project)
1025 # Copy users first, then groups to handle members with inherited and given roles
1028 # Copy users first, then groups to handle members with inherited and given roles
1026 members_to_copy = []
1029 members_to_copy = []
1027 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1030 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1028 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1031 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1029
1032
1030 members_to_copy.each do |member|
1033 members_to_copy.each do |member|
1031 new_member = Member.new
1034 new_member = Member.new
1032 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1035 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1033 # only copy non inherited roles
1036 # only copy non inherited roles
1034 # inherited roles will be added when copying the group membership
1037 # inherited roles will be added when copying the group membership
1035 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1038 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1036 next if role_ids.empty?
1039 next if role_ids.empty?
1037 new_member.role_ids = role_ids
1040 new_member.role_ids = role_ids
1038 new_member.project = self
1041 new_member.project = self
1039 self.members << new_member
1042 self.members << new_member
1040 end
1043 end
1041 end
1044 end
1042
1045
1043 # Copies queries from +project+
1046 # Copies queries from +project+
1044 def copy_queries(project)
1047 def copy_queries(project)
1045 project.queries.each do |query|
1048 project.queries.each do |query|
1046 new_query = IssueQuery.new
1049 new_query = IssueQuery.new
1047 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1050 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1048 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1051 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1049 new_query.project = self
1052 new_query.project = self
1050 new_query.user_id = query.user_id
1053 new_query.user_id = query.user_id
1051 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1054 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1052 self.queries << new_query
1055 self.queries << new_query
1053 end
1056 end
1054 end
1057 end
1055
1058
1056 # Copies boards from +project+
1059 # Copies boards from +project+
1057 def copy_boards(project)
1060 def copy_boards(project)
1058 project.boards.each do |board|
1061 project.boards.each do |board|
1059 new_board = Board.new
1062 new_board = Board.new
1060 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1063 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1061 new_board.project = self
1064 new_board.project = self
1062 self.boards << new_board
1065 self.boards << new_board
1063 end
1066 end
1064 end
1067 end
1065
1068
1066 def allowed_permissions
1069 def allowed_permissions
1067 @allowed_permissions ||= begin
1070 @allowed_permissions ||= begin
1068 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1071 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1069 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1072 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1070 end
1073 end
1071 end
1074 end
1072
1075
1073 def allowed_actions
1076 def allowed_actions
1074 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1077 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1075 end
1078 end
1076
1079
1077 # Archives subprojects recursively
1080 # Archives subprojects recursively
1078 def archive!
1081 def archive!
1079 children.each do |subproject|
1082 children.each do |subproject|
1080 subproject.send :archive!
1083 subproject.send :archive!
1081 end
1084 end
1082 update_attribute :status, STATUS_ARCHIVED
1085 update_attribute :status, STATUS_ARCHIVED
1083 end
1086 end
1084 end
1087 end
@@ -1,43 +1,44
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_project_new), new_project_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_project_new), new_project_path, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <%= title l(:label_project_plural) %>
5 <%= title l(:label_project_plural) %>
6
6
7 <%= form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %> :</label>
9 <label for='status'><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 <label for='name'><%= l(:label_project) %>:</label>
11 <label for='name'><%= l(:label_project) %>:</label>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 <%= link_to l(:button_clear), admin_projects_path, :class => 'icon icon-reload' %>
14 <%= link_to l(:button_clear), admin_projects_path, :class => 'icon icon-reload' %>
15 </fieldset>
15 </fieldset>
16 <% end %>
16 <% end %>
17 &nbsp;
17 &nbsp;
18
18
19 <div class="autoscroll">
19 <div class="autoscroll">
20 <table class="list">
20 <table class="list">
21 <thead><tr>
21 <thead><tr>
22 <th><%=l(:label_project)%></th>
22 <th><%=l(:label_project)%></th>
23 <th><%=l(:field_is_public)%></th>
23 <th><%=l(:field_is_public)%></th>
24 <th><%=l(:field_created_on)%></th>
24 <th><%=l(:field_created_on)%></th>
25 <th></th>
25 <th></th>
26 </tr></thead>
26 </tr></thead>
27 <tbody>
27 <tbody>
28 <% project_tree(@projects) do |project, level| %>
28 <% project_tree(@projects, :init_level => true) do |project, level| %>
29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 <td class="name"><span><%= link_to_project_settings(project, {}, :title => project.short_description) %></span></td>
30 <td class="name"><span><%= link_to_project_settings(project, {}, :title => project.short_description) %></span></td>
31 <td><%= checked_image project.is_public? %></td>
31 <td><%= checked_image project.is_public? %></td>
32 <td><%= format_date(project.created_on) %></td>
32 <td><%= format_date(project.created_on) %></td>
33 <td class="buttons">
33 <td class="buttons">
34 <%= link_to(l(:button_archive), archive_project_path(project, :status => params[:status]), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %>
34 <%= link_to(l(:button_archive), archive_project_path(project, :status => params[:status]), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %>
35 <%= link_to(l(:button_unarchive), unarchive_project_path(project, :status => params[:status]), :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %>
35 <%= link_to(l(:button_unarchive), unarchive_project_path(project, :status => params[:status]), :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %>
36 <%= link_to(l(:button_copy), copy_project_path(project), :class => 'icon icon-copy') %>
36 <%= link_to(l(:button_copy), copy_project_path(project), :class => 'icon icon-copy') %>
37 <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
37 <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
38 </td>
38 </td>
39 </tr>
39 </tr>
40 <% end %>
40 <% end %>
41 </tbody>
41 </tbody>
42 </table>
42 </table>
43 </div>
43 </div>
44 <span class="pagination"><%= pagination_links_full @project_pages, @project_count %></span> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now