##// END OF EJS Templates
Declare safe attributes for User and Projects models....
Jean-Philippe Lang -
r4378:a4d7a99c22d9
parent child
Show More
@@ -1,179 +1,179
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 MyController < ApplicationController
18 class MyController < ApplicationController
19 before_filter :require_login
19 before_filter :require_login
20
20
21 helper :issues
21 helper :issues
22 helper :custom_fields
22 helper :custom_fields
23
23
24 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
24 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
25 'issuesreportedbyme' => :label_reported_issues,
25 'issuesreportedbyme' => :label_reported_issues,
26 'issueswatched' => :label_watched_issues,
26 'issueswatched' => :label_watched_issues,
27 'news' => :label_news_latest,
27 'news' => :label_news_latest,
28 'calendar' => :label_calendar,
28 'calendar' => :label_calendar,
29 'documents' => :label_document_plural,
29 'documents' => :label_document_plural,
30 'timelog' => :label_spent_time
30 'timelog' => :label_spent_time
31 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
31 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
32
32
33 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
33 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
34 'right' => ['issuesreportedbyme']
34 'right' => ['issuesreportedbyme']
35 }.freeze
35 }.freeze
36
36
37 verify :xhr => true,
37 verify :xhr => true,
38 :only => [:add_block, :remove_block, :order_blocks]
38 :only => [:add_block, :remove_block, :order_blocks]
39
39
40 def index
40 def index
41 page
41 page
42 render :action => 'page'
42 render :action => 'page'
43 end
43 end
44
44
45 # Show user's page
45 # Show user's page
46 def page
46 def page
47 @user = User.current
47 @user = User.current
48 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
48 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
49 end
49 end
50
50
51 # Edit user's account
51 # Edit user's account
52 def account
52 def account
53 @user = User.current
53 @user = User.current
54 @pref = @user.pref
54 @pref = @user.pref
55 if request.post?
55 if request.post?
56 @user.attributes = params[:user]
56 @user.safe_attributes = params[:user]
57 @user.mail_notification = params[:notification_option] || 'only_my_events'
57 @user.mail_notification = params[:notification_option] || 'only_my_events'
58 @user.pref.attributes = params[:pref]
58 @user.pref.attributes = params[:pref]
59 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
59 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
60 if @user.save
60 if @user.save
61 @user.pref.save
61 @user.pref.save
62 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
62 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
63 set_language_if_valid @user.language
63 set_language_if_valid @user.language
64 flash[:notice] = l(:notice_account_updated)
64 flash[:notice] = l(:notice_account_updated)
65 redirect_to :action => 'account'
65 redirect_to :action => 'account'
66 return
66 return
67 end
67 end
68 end
68 end
69 @notification_options = @user.valid_notification_options
69 @notification_options = @user.valid_notification_options
70 @notification_option = @user.mail_notification #? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
70 @notification_option = @user.mail_notification #? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
71 end
71 end
72
72
73 # Manage user's password
73 # Manage user's password
74 def password
74 def password
75 @user = User.current
75 @user = User.current
76 unless @user.change_password_allowed?
76 unless @user.change_password_allowed?
77 flash[:error] = l(:notice_can_t_change_password)
77 flash[:error] = l(:notice_can_t_change_password)
78 redirect_to :action => 'account'
78 redirect_to :action => 'account'
79 return
79 return
80 end
80 end
81 if request.post?
81 if request.post?
82 if @user.check_password?(params[:password])
82 if @user.check_password?(params[:password])
83 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
83 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
84 if @user.save
84 if @user.save
85 flash[:notice] = l(:notice_account_password_updated)
85 flash[:notice] = l(:notice_account_password_updated)
86 redirect_to :action => 'account'
86 redirect_to :action => 'account'
87 end
87 end
88 else
88 else
89 flash[:error] = l(:notice_account_wrong_password)
89 flash[:error] = l(:notice_account_wrong_password)
90 end
90 end
91 end
91 end
92 end
92 end
93
93
94 # Create a new feeds key
94 # Create a new feeds key
95 def reset_rss_key
95 def reset_rss_key
96 if request.post?
96 if request.post?
97 if User.current.rss_token
97 if User.current.rss_token
98 User.current.rss_token.destroy
98 User.current.rss_token.destroy
99 User.current.reload
99 User.current.reload
100 end
100 end
101 User.current.rss_key
101 User.current.rss_key
102 flash[:notice] = l(:notice_feeds_access_key_reseted)
102 flash[:notice] = l(:notice_feeds_access_key_reseted)
103 end
103 end
104 redirect_to :action => 'account'
104 redirect_to :action => 'account'
105 end
105 end
106
106
107 # Create a new API key
107 # Create a new API key
108 def reset_api_key
108 def reset_api_key
109 if request.post?
109 if request.post?
110 if User.current.api_token
110 if User.current.api_token
111 User.current.api_token.destroy
111 User.current.api_token.destroy
112 User.current.reload
112 User.current.reload
113 end
113 end
114 User.current.api_key
114 User.current.api_key
115 flash[:notice] = l(:notice_api_access_key_reseted)
115 flash[:notice] = l(:notice_api_access_key_reseted)
116 end
116 end
117 redirect_to :action => 'account'
117 redirect_to :action => 'account'
118 end
118 end
119
119
120 # User's page layout configuration
120 # User's page layout configuration
121 def page_layout
121 def page_layout
122 @user = User.current
122 @user = User.current
123 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
123 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
124 @block_options = []
124 @block_options = []
125 BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
125 BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
126 end
126 end
127
127
128 # Add a block to user's page
128 # Add a block to user's page
129 # The block is added on top of the page
129 # The block is added on top of the page
130 # params[:block] : id of the block to add
130 # params[:block] : id of the block to add
131 def add_block
131 def add_block
132 block = params[:block].to_s.underscore
132 block = params[:block].to_s.underscore
133 (render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
133 (render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
134 @user = User.current
134 @user = User.current
135 layout = @user.pref[:my_page_layout] || {}
135 layout = @user.pref[:my_page_layout] || {}
136 # remove if already present in a group
136 # remove if already present in a group
137 %w(top left right).each {|f| (layout[f] ||= []).delete block }
137 %w(top left right).each {|f| (layout[f] ||= []).delete block }
138 # add it on top
138 # add it on top
139 layout['top'].unshift block
139 layout['top'].unshift block
140 @user.pref[:my_page_layout] = layout
140 @user.pref[:my_page_layout] = layout
141 @user.pref.save
141 @user.pref.save
142 render :partial => "block", :locals => {:user => @user, :block_name => block}
142 render :partial => "block", :locals => {:user => @user, :block_name => block}
143 end
143 end
144
144
145 # Remove a block to user's page
145 # Remove a block to user's page
146 # params[:block] : id of the block to remove
146 # params[:block] : id of the block to remove
147 def remove_block
147 def remove_block
148 block = params[:block].to_s.underscore
148 block = params[:block].to_s.underscore
149 @user = User.current
149 @user = User.current
150 # remove block in all groups
150 # remove block in all groups
151 layout = @user.pref[:my_page_layout] || {}
151 layout = @user.pref[:my_page_layout] || {}
152 %w(top left right).each {|f| (layout[f] ||= []).delete block }
152 %w(top left right).each {|f| (layout[f] ||= []).delete block }
153 @user.pref[:my_page_layout] = layout
153 @user.pref[:my_page_layout] = layout
154 @user.pref.save
154 @user.pref.save
155 render :nothing => true
155 render :nothing => true
156 end
156 end
157
157
158 # Change blocks order on user's page
158 # Change blocks order on user's page
159 # params[:group] : group to order (top, left or right)
159 # params[:group] : group to order (top, left or right)
160 # params[:list-(top|left|right)] : array of block ids of the group
160 # params[:list-(top|left|right)] : array of block ids of the group
161 def order_blocks
161 def order_blocks
162 group = params[:group]
162 group = params[:group]
163 @user = User.current
163 @user = User.current
164 if group.is_a?(String)
164 if group.is_a?(String)
165 group_items = (params["list-#{group}"] || []).collect(&:underscore)
165 group_items = (params["list-#{group}"] || []).collect(&:underscore)
166 if group_items and group_items.is_a? Array
166 if group_items and group_items.is_a? Array
167 layout = @user.pref[:my_page_layout] || {}
167 layout = @user.pref[:my_page_layout] || {}
168 # remove group blocks if they are presents in other groups
168 # remove group blocks if they are presents in other groups
169 %w(top left right).each {|f|
169 %w(top left right).each {|f|
170 layout[f] = (layout[f] || []) - group_items
170 layout[f] = (layout[f] || []) - group_items
171 }
171 }
172 layout[group] = group_items
172 layout[group] = group_items
173 @user.pref[:my_page_layout] = layout
173 @user.pref[:my_page_layout] = layout
174 @user.pref.save
174 @user.pref.save
175 end
175 end
176 end
176 end
177 render :nothing => true
177 render :nothing => true
178 end
178 end
179 end
179 end
@@ -1,266 +1,268
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_key_auth :index, :show, :create, :update, :destroy
27 accept_key_auth :index, :show, :create, :update, :destroy
28
28
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 if controller.request.post?
30 if controller.request.post?
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 end
32 end
33 end
33 end
34
34
35 # TODO: convert to PUT only
35 # TODO: convert to PUT only
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper :queries
43 helper :queries
44 include QueriesHelper
44 include QueriesHelper
45 helper :repositories
45 helper :repositories
46 include RepositoriesHelper
46 include RepositoriesHelper
47 include ProjectsHelper
47 include ProjectsHelper
48
48
49 # Lists visible projects
49 # Lists visible projects
50 def index
50 def index
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @projects = Project.visible.find(:all, :order => 'lft')
53 @projects = Project.visible.find(:all, :order => 'lft')
54 }
54 }
55 format.api {
55 format.api {
56 @projects = Project.visible.find(:all, :order => 'lft')
56 @projects = Project.visible.find(:all, :order => 'lft')
57 }
57 }
58 format.atom {
58 format.atom {
59 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 projects = Project.visible.find(:all, :order => 'created_on DESC',
60 :limit => Setting.feeds_limit.to_i)
60 :limit => Setting.feeds_limit.to_i)
61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
62 }
62 }
63 end
63 end
64 end
64 end
65
65
66 def new
66 def new
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
68 @trackers = Tracker.all
68 @trackers = Tracker.all
69 @project = Project.new(params[:project])
69 @project = Project.new(params[:project])
70 end
70 end
71
71
72 def create
72 def create
73 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
73 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
74 @trackers = Tracker.all
74 @trackers = Tracker.all
75 @project = Project.new(params[:project])
75 @project = Project.new
76 @project.safe_attributes = params[:project]
76
77
77 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
78 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
78 if validate_parent_id && @project.save
79 if validate_parent_id && @project.save
79 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 # Add current user as a project member if he is not admin
81 # Add current user as a project member if he is not admin
81 unless User.current.admin?
82 unless User.current.admin?
82 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 m = Member.new(:user => User.current, :roles => [r])
84 m = Member.new(:user => User.current, :roles => [r])
84 @project.members << m
85 @project.members << m
85 end
86 end
86 respond_to do |format|
87 respond_to do |format|
87 format.html {
88 format.html {
88 flash[:notice] = l(:notice_successful_create)
89 flash[:notice] = l(:notice_successful_create)
89 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 }
91 }
91 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 end
93 end
93 else
94 else
94 respond_to do |format|
95 respond_to do |format|
95 format.html { render :action => 'new' }
96 format.html { render :action => 'new' }
96 format.api { render_validation_errors(@project) }
97 format.api { render_validation_errors(@project) }
97 end
98 end
98 end
99 end
99
100
100 end
101 end
101
102
102 def copy
103 def copy
103 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 @trackers = Tracker.all
105 @trackers = Tracker.all
105 @root_projects = Project.find(:all,
106 @root_projects = Project.find(:all,
106 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 :order => 'name')
108 :order => 'name')
108 @source_project = Project.find(params[:id])
109 @source_project = Project.find(params[:id])
109 if request.get?
110 if request.get?
110 @project = Project.copy_from(@source_project)
111 @project = Project.copy_from(@source_project)
111 if @project
112 if @project
112 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 else
114 else
114 redirect_to :controller => 'admin', :action => 'projects'
115 redirect_to :controller => 'admin', :action => 'projects'
115 end
116 end
116 else
117 else
117 Mailer.with_deliveries(params[:notifications] == '1') do
118 Mailer.with_deliveries(params[:notifications] == '1') do
118 @project = Project.new(params[:project])
119 @project = Project.new
120 @project.safe_attributes = params[:project]
119 @project.enabled_module_names = params[:enabled_modules]
121 @project.enabled_module_names = params[:enabled_modules]
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
122 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
123 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 flash[:notice] = l(:notice_successful_create)
124 flash[:notice] = l(:notice_successful_create)
123 redirect_to :controller => 'projects', :action => 'settings'
125 redirect_to :controller => 'projects', :action => 'settings'
124 elsif !@project.new_record?
126 elsif !@project.new_record?
125 # Project was created
127 # Project was created
126 # But some objects were not copied due to validation failures
128 # But some objects were not copied due to validation failures
127 # (eg. issues from disabled trackers)
129 # (eg. issues from disabled trackers)
128 # TODO: inform about that
130 # TODO: inform about that
129 redirect_to :controller => 'projects', :action => 'settings'
131 redirect_to :controller => 'projects', :action => 'settings'
130 end
132 end
131 end
133 end
132 end
134 end
133 rescue ActiveRecord::RecordNotFound
135 rescue ActiveRecord::RecordNotFound
134 redirect_to :controller => 'admin', :action => 'projects'
136 redirect_to :controller => 'admin', :action => 'projects'
135 end
137 end
136
138
137 # Show @project
139 # Show @project
138 def show
140 def show
139 if params[:jump]
141 if params[:jump]
140 # try to redirect to the requested menu item
142 # try to redirect to the requested menu item
141 redirect_to_project_menu_item(@project, params[:jump]) && return
143 redirect_to_project_menu_item(@project, params[:jump]) && return
142 end
144 end
143
145
144 @users_by_role = @project.users_by_role
146 @users_by_role = @project.users_by_role
145 @subprojects = @project.children.visible
147 @subprojects = @project.children.visible
146 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
148 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 @trackers = @project.rolled_up_trackers
149 @trackers = @project.rolled_up_trackers
148
150
149 cond = @project.project_condition(Setting.display_subprojects_issues?)
151 cond = @project.project_condition(Setting.display_subprojects_issues?)
150
152
151 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
153 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 :include => [:project, :status, :tracker],
154 :include => [:project, :status, :tracker],
153 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
155 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
156 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 :include => [:project, :status, :tracker],
157 :include => [:project, :status, :tracker],
156 :conditions => cond)
158 :conditions => cond)
157
159
158 TimeEntry.visible_by(User.current) do
160 TimeEntry.visible_by(User.current) do
159 @total_hours = TimeEntry.sum(:hours,
161 @total_hours = TimeEntry.sum(:hours,
160 :include => :project,
162 :include => :project,
161 :conditions => cond).to_f
163 :conditions => cond).to_f
162 end
164 end
163 @key = User.current.rss_key
165 @key = User.current.rss_key
164
166
165 respond_to do |format|
167 respond_to do |format|
166 format.html
168 format.html
167 format.api
169 format.api
168 end
170 end
169 end
171 end
170
172
171 def settings
173 def settings
172 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
174 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
173 @issue_category ||= IssueCategory.new
175 @issue_category ||= IssueCategory.new
174 @member ||= @project.members.new
176 @member ||= @project.members.new
175 @trackers = Tracker.all
177 @trackers = Tracker.all
176 @repository ||= @project.repository
178 @repository ||= @project.repository
177 @wiki ||= @project.wiki
179 @wiki ||= @project.wiki
178 end
180 end
179
181
180 def edit
182 def edit
181 end
183 end
182
184
183 def update
185 def update
184 @project.attributes = params[:project]
186 @project.safe_attributes = params[:project]
185 if validate_parent_id && @project.save
187 if validate_parent_id && @project.save
186 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
188 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
187 respond_to do |format|
189 respond_to do |format|
188 format.html {
190 format.html {
189 flash[:notice] = l(:notice_successful_update)
191 flash[:notice] = l(:notice_successful_update)
190 redirect_to :action => 'settings', :id => @project
192 redirect_to :action => 'settings', :id => @project
191 }
193 }
192 format.api { head :ok }
194 format.api { head :ok }
193 end
195 end
194 else
196 else
195 respond_to do |format|
197 respond_to do |format|
196 format.html {
198 format.html {
197 settings
199 settings
198 render :action => 'settings'
200 render :action => 'settings'
199 }
201 }
200 format.api { render_validation_errors(@project) }
202 format.api { render_validation_errors(@project) }
201 end
203 end
202 end
204 end
203 end
205 end
204
206
205 def modules
207 def modules
206 @project.enabled_module_names = params[:enabled_modules]
208 @project.enabled_module_names = params[:enabled_modules]
207 flash[:notice] = l(:notice_successful_update)
209 flash[:notice] = l(:notice_successful_update)
208 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
210 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
209 end
211 end
210
212
211 def archive
213 def archive
212 if request.post?
214 if request.post?
213 unless @project.archive
215 unless @project.archive
214 flash[:error] = l(:error_can_not_archive_project)
216 flash[:error] = l(:error_can_not_archive_project)
215 end
217 end
216 end
218 end
217 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
219 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
218 end
220 end
219
221
220 def unarchive
222 def unarchive
221 @project.unarchive if request.post? && !@project.active?
223 @project.unarchive if request.post? && !@project.active?
222 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
223 end
225 end
224
226
225 # Delete @project
227 # Delete @project
226 def destroy
228 def destroy
227 @project_to_destroy = @project
229 @project_to_destroy = @project
228 if request.get?
230 if request.get?
229 # display confirmation view
231 # display confirmation view
230 else
232 else
231 if api_request? || params[:confirm]
233 if api_request? || params[:confirm]
232 @project_to_destroy.destroy
234 @project_to_destroy.destroy
233 respond_to do |format|
235 respond_to do |format|
234 format.html { redirect_to :controller => 'admin', :action => 'projects' }
236 format.html { redirect_to :controller => 'admin', :action => 'projects' }
235 format.api { head :ok }
237 format.api { head :ok }
236 end
238 end
237 end
239 end
238 end
240 end
239 # hide project in layout
241 # hide project in layout
240 @project = nil
242 @project = nil
241 end
243 end
242
244
243 private
245 private
244 def find_optional_project
246 def find_optional_project
245 return true unless params[:id]
247 return true unless params[:id]
246 @project = Project.find(params[:id])
248 @project = Project.find(params[:id])
247 authorize
249 authorize
248 rescue ActiveRecord::RecordNotFound
250 rescue ActiveRecord::RecordNotFound
249 render_404
251 render_404
250 end
252 end
251
253
252 # Validates parent_id param according to user's permissions
254 # Validates parent_id param according to user's permissions
253 # TODO: move it to Project model in a validation that depends on User.current
255 # TODO: move it to Project model in a validation that depends on User.current
254 def validate_parent_id
256 def validate_parent_id
255 return true if User.current.admin?
257 return true if User.current.admin?
256 parent_id = params[:project] && params[:project][:parent_id]
258 parent_id = params[:project] && params[:project][:parent_id]
257 if parent_id || @project.new_record?
259 if parent_id || @project.new_record?
258 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
260 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
259 unless @project.allowed_parents.include?(parent)
261 unless @project.allowed_parents.include?(parent)
260 @project.errors.add :parent_id, :invalid
262 @project.errors.add :parent_id, :invalid
261 return false
263 return false
262 end
264 end
263 end
265 end
264 true
266 true
265 end
267 end
266 end
268 end
@@ -1,230 +1,231
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :show
21 before_filter :require_admin, :except => :show
22 accept_key_auth :index, :show, :create, :update
22 accept_key_auth :index, :show, :create, :update
23
23
24 helper :sort
24 helper :sort
25 include SortHelper
25 include SortHelper
26 helper :custom_fields
26 helper :custom_fields
27 include CustomFieldsHelper
27 include CustomFieldsHelper
28
28
29 def index
29 def index
30 sort_init 'login', 'asc'
30 sort_init 'login', 'asc'
31 sort_update %w(login firstname lastname mail admin created_on last_login_on)
31 sort_update %w(login firstname lastname mail admin created_on last_login_on)
32
32
33 case params[:format]
33 case params[:format]
34 when 'xml', 'json'
34 when 'xml', 'json'
35 @offset, @limit = api_offset_and_limit
35 @offset, @limit = api_offset_and_limit
36 else
36 else
37 @limit = per_page_option
37 @limit = per_page_option
38 end
38 end
39
39
40 @status = params[:status] ? params[:status].to_i : 1
40 @status = params[:status] ? params[:status].to_i : 1
41 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
41 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
42
42
43 unless params[:name].blank?
43 unless params[:name].blank?
44 name = "%#{params[:name].strip.downcase}%"
44 name = "%#{params[:name].strip.downcase}%"
45 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
45 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
46 end
46 end
47
47
48 @user_count = User.count(:conditions => c.conditions)
48 @user_count = User.count(:conditions => c.conditions)
49 @user_pages = Paginator.new self, @user_count, @limit, params['page']
49 @user_pages = Paginator.new self, @user_count, @limit, params['page']
50 @offset ||= @user_pages.current.offset
50 @offset ||= @user_pages.current.offset
51 @users = User.find :all,
51 @users = User.find :all,
52 :order => sort_clause,
52 :order => sort_clause,
53 :conditions => c.conditions,
53 :conditions => c.conditions,
54 :limit => @limit,
54 :limit => @limit,
55 :offset => @offset
55 :offset => @offset
56
56
57 respond_to do |format|
57 respond_to do |format|
58 format.html { render :layout => !request.xhr? }
58 format.html { render :layout => !request.xhr? }
59 format.api
59 format.api
60 end
60 end
61 end
61 end
62
62
63 def show
63 def show
64 @user = User.find(params[:id])
64 @user = User.find(params[:id])
65
65
66 # show projects based on current user visibility
66 # show projects based on current user visibility
67 @memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
67 @memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
68
68
69 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
69 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
70 @events_by_day = events.group_by(&:event_date)
70 @events_by_day = events.group_by(&:event_date)
71
71
72 unless User.current.admin?
72 unless User.current.admin?
73 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
73 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
74 render_404
74 render_404
75 return
75 return
76 end
76 end
77 end
77 end
78
78
79 respond_to do |format|
79 respond_to do |format|
80 format.html { render :layout => 'base' }
80 format.html { render :layout => 'base' }
81 format.api
81 format.api
82 end
82 end
83 rescue ActiveRecord::RecordNotFound
83 rescue ActiveRecord::RecordNotFound
84 render_404
84 render_404
85 end
85 end
86
86
87 def new
87 def new
88 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
88 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
89 @notification_option = Setting.default_notification_option
89 @notification_option = Setting.default_notification_option
90
90
91 @user = User.new(:language => Setting.default_language)
91 @user = User.new(:language => Setting.default_language)
92 @auth_sources = AuthSource.find(:all)
92 @auth_sources = AuthSource.find(:all)
93 end
93 end
94
94
95 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
95 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
96 def create
96 def create
97 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
97 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
98 @notification_option = Setting.default_notification_option
98 @notification_option = Setting.default_notification_option
99
99
100 @user = User.new(params[:user])
100 @user = User.new
101 @user.safe_attributes = params[:user]
101 @user.admin = params[:user][:admin] || false
102 @user.admin = params[:user][:admin] || false
102 @user.login = params[:user][:login]
103 @user.login = params[:user][:login]
103 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
104 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
104
105
105 # TODO: Similar to My#account
106 # TODO: Similar to My#account
106 @user.mail_notification = params[:notification_option] || 'only_my_events'
107 @user.mail_notification = params[:notification_option] || 'only_my_events'
107 @user.pref.attributes = params[:pref]
108 @user.pref.attributes = params[:pref]
108 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
109 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
109
110
110 if @user.save
111 if @user.save
111 @user.pref.save
112 @user.pref.save
112 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
113 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
113
114
114 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
115 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
115
116
116 respond_to do |format|
117 respond_to do |format|
117 format.html {
118 format.html {
118 flash[:notice] = l(:notice_successful_create)
119 flash[:notice] = l(:notice_successful_create)
119 redirect_to(params[:continue] ?
120 redirect_to(params[:continue] ?
120 {:controller => 'users', :action => 'new'} :
121 {:controller => 'users', :action => 'new'} :
121 {:controller => 'users', :action => 'edit', :id => @user}
122 {:controller => 'users', :action => 'edit', :id => @user}
122 )
123 )
123 }
124 }
124 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
125 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
125 end
126 end
126 else
127 else
127 @auth_sources = AuthSource.find(:all)
128 @auth_sources = AuthSource.find(:all)
128 @notification_option = @user.mail_notification
129 @notification_option = @user.mail_notification
129
130
130 respond_to do |format|
131 respond_to do |format|
131 format.html { render :action => 'new' }
132 format.html { render :action => 'new' }
132 format.api { render_validation_errors(@user) }
133 format.api { render_validation_errors(@user) }
133 end
134 end
134 end
135 end
135 end
136 end
136
137
137 def edit
138 def edit
138 @user = User.find(params[:id])
139 @user = User.find(params[:id])
139 @notification_options = @user.valid_notification_options
140 @notification_options = @user.valid_notification_options
140 @notification_option = @user.mail_notification
141 @notification_option = @user.mail_notification
141
142
142 @auth_sources = AuthSource.find(:all)
143 @auth_sources = AuthSource.find(:all)
143 @membership ||= Member.new
144 @membership ||= Member.new
144 end
145 end
145
146
146 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
147 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
147 def update
148 def update
148 @user = User.find(params[:id])
149 @user = User.find(params[:id])
149 @notification_options = @user.valid_notification_options
150 @notification_options = @user.valid_notification_options
150 @notification_option = @user.mail_notification
151 @notification_option = @user.mail_notification
151
152
152 @user.admin = params[:user][:admin] if params[:user][:admin]
153 @user.admin = params[:user][:admin] if params[:user][:admin]
153 @user.login = params[:user][:login] if params[:user][:login]
154 @user.login = params[:user][:login] if params[:user][:login]
154 if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
155 if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
155 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
156 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
156 end
157 end
157 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
158 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
158 @user.attributes = params[:user]
159 @user.safe_attributes = params[:user]
159 # Was the account actived ? (do it before User#save clears the change)
160 # Was the account actived ? (do it before User#save clears the change)
160 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
161 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
161 # TODO: Similar to My#account
162 # TODO: Similar to My#account
162 @user.mail_notification = params[:notification_option] || 'only_my_events'
163 @user.mail_notification = params[:notification_option] || 'only_my_events'
163 @user.pref.attributes = params[:pref]
164 @user.pref.attributes = params[:pref]
164 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
165 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
165
166
166 if @user.save
167 if @user.save
167 @user.pref.save
168 @user.pref.save
168 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
169 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
169
170
170 if was_activated
171 if was_activated
171 Mailer.deliver_account_activated(@user)
172 Mailer.deliver_account_activated(@user)
172 elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
173 elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
173 Mailer.deliver_account_information(@user, params[:password])
174 Mailer.deliver_account_information(@user, params[:password])
174 end
175 end
175
176
176 respond_to do |format|
177 respond_to do |format|
177 format.html {
178 format.html {
178 flash[:notice] = l(:notice_successful_update)
179 flash[:notice] = l(:notice_successful_update)
179 redirect_to :back
180 redirect_to :back
180 }
181 }
181 format.api { head :ok }
182 format.api { head :ok }
182 end
183 end
183 else
184 else
184 @auth_sources = AuthSource.find(:all)
185 @auth_sources = AuthSource.find(:all)
185 @membership ||= Member.new
186 @membership ||= Member.new
186
187
187 respond_to do |format|
188 respond_to do |format|
188 format.html { render :action => :edit }
189 format.html { render :action => :edit }
189 format.api { render_validation_errors(@user) }
190 format.api { render_validation_errors(@user) }
190 end
191 end
191 end
192 end
192 rescue ::ActionController::RedirectBackError
193 rescue ::ActionController::RedirectBackError
193 redirect_to :controller => 'users', :action => 'edit', :id => @user
194 redirect_to :controller => 'users', :action => 'edit', :id => @user
194 end
195 end
195
196
196 def edit_membership
197 def edit_membership
197 @user = User.find(params[:id])
198 @user = User.find(params[:id])
198 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
199 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
199 @membership.save if request.post?
200 @membership.save if request.post?
200 respond_to do |format|
201 respond_to do |format|
201 if @membership.valid?
202 if @membership.valid?
202 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
203 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
203 format.js {
204 format.js {
204 render(:update) {|page|
205 render(:update) {|page|
205 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
206 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
206 page.visual_effect(:highlight, "member-#{@membership.id}")
207 page.visual_effect(:highlight, "member-#{@membership.id}")
207 }
208 }
208 }
209 }
209 else
210 else
210 format.js {
211 format.js {
211 render(:update) {|page|
212 render(:update) {|page|
212 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
213 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
213 }
214 }
214 }
215 }
215 end
216 end
216 end
217 end
217 end
218 end
218
219
219 def destroy_membership
220 def destroy_membership
220 @user = User.find(params[:id])
221 @user = User.find(params[:id])
221 @membership = Member.find(params[:membership_id])
222 @membership = Member.find(params[:membership_id])
222 if request.post? && @membership.deletable?
223 if request.post? && @membership.deletable?
223 @membership.destroy
224 @membership.destroy
224 end
225 end
225 respond_to do |format|
226 respond_to do |format|
226 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
227 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
227 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
228 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
228 end
229 end
229 end
230 end
230 end
231 end
@@ -1,821 +1,832
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
20
19 # Project statuses
21 # Project statuses
20 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
22
24
23 # Maximum length for project identifiers
25 # Maximum length for project identifiers
24 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
25
27
26 # Specific overidden Activities
28 # Specific overidden Activities
27 has_many :time_entry_activities
29 has_many :time_entry_activities
28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
30 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
31 :include => :principal,
33 :include => :principal,
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 has_many :users, :through => :members
35 has_many :users, :through => :members
34 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
35
37
36 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author
46 has_many :news, :dependent => :delete_all, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
50 # Custom field for the project issues
52 # Custom field for the project issues
51 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
52 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
56
58
57 acts_as_nested_set :order => 'name'
59 acts_as_nested_set :order => 'name'
58 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files
61 :delete_permission => :manage_files
60
62
61 acts_as_customizable
63 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
63 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}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
65 :author => nil
67 :author => nil
66
68
67 attr_protected :status, :enabled_module_names
69 attr_protected :status, :enabled_module_names
68
70
69 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
70 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
71 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words
79 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
79
81
80 before_destroy :delete_all_members, :destroy_children
82 before_destroy :delete_all_members, :destroy_children
81
83
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86
88
87 def initialize(attributes = nil)
89 def initialize(attributes = nil)
88 super
90 super
89
91
90 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
91 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
92 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
93 end
95 end
94 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
95 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
96 end
98 end
97 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
98 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
99 end
101 end
100 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
101 self.trackers = Tracker.all
103 self.trackers = Tracker.all
102 end
104 end
103 end
105 end
104
106
105 def identifier=(identifier)
107 def identifier=(identifier)
106 super unless identifier_frozen?
108 super unless identifier_frozen?
107 end
109 end
108
110
109 def identifier_frozen?
111 def identifier_frozen?
110 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
111 end
113 end
112
114
113 # returns latest created projects
115 # returns latest created projects
114 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
115 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
116 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
117 end
119 end
118
120
119 # Returns a SQL :conditions string used to find all active projects for the specified user.
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
120 #
122 #
121 # Examples:
123 # Examples:
122 # Projects.visible_by(admin) => "projects.status = 1"
124 # Projects.visible_by(admin) => "projects.status = 1"
123 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
124 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
125 user ||= User.current
127 user ||= User.current
126 if user && user.admin?
128 if user && user.admin?
127 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
128 elsif user && user.memberships.any?
130 elsif user && user.memberships.any?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
130 else
132 else
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
132 end
134 end
133 end
135 end
134
136
135 def self.allowed_to_condition(user, permission, options={})
137 def self.allowed_to_condition(user, permission, options={})
136 statements = []
138 statements = []
137 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
138 if perm = Redmine::AccessControl.permission(permission)
140 if perm = Redmine::AccessControl.permission(permission)
139 unless perm.project_module.nil?
141 unless perm.project_module.nil?
140 # If the permission belongs to a project module, make sure the module is enabled
142 # If the permission belongs to a project module, make sure the module is enabled
141 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
142 end
144 end
143 end
145 end
144 if options[:project]
146 if options[:project]
145 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
146 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
147 base_statement = "(#{project_statement}) AND (#{base_statement})"
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
148 end
150 end
149 if user.admin?
151 if user.admin?
150 # no restriction
152 # no restriction
151 else
153 else
152 statements << "1=0"
154 statements << "1=0"
153 if user.logged?
155 if user.logged?
154 if Role.non_member.allowed_to?(permission) && !options[:member]
156 if Role.non_member.allowed_to?(permission) && !options[:member]
155 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
156 end
158 end
157 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
158 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
159 else
161 else
160 if Role.anonymous.allowed_to?(permission) && !options[:member]
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
161 # anonymous user allowed on public project
163 # anonymous user allowed on public project
162 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
163 end
165 end
164 end
166 end
165 end
167 end
166 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
167 end
169 end
168
170
169 # Returns the Systemwide and project specific activities
171 # Returns the Systemwide and project specific activities
170 def activities(include_inactive=false)
172 def activities(include_inactive=false)
171 if include_inactive
173 if include_inactive
172 return all_activities
174 return all_activities
173 else
175 else
174 return active_activities
176 return active_activities
175 end
177 end
176 end
178 end
177
179
178 # Will create a new Project specific Activity or update an existing one
180 # Will create a new Project specific Activity or update an existing one
179 #
181 #
180 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
181 # does not successfully save.
183 # does not successfully save.
182 def update_or_create_time_entry_activity(id, activity_hash)
184 def update_or_create_time_entry_activity(id, activity_hash)
183 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
184 self.create_time_entry_activity_if_needed(activity_hash)
186 self.create_time_entry_activity_if_needed(activity_hash)
185 else
187 else
186 activity = project.time_entry_activities.find_by_id(id.to_i)
188 activity = project.time_entry_activities.find_by_id(id.to_i)
187 activity.update_attributes(activity_hash) if activity
189 activity.update_attributes(activity_hash) if activity
188 end
190 end
189 end
191 end
190
192
191 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
192 #
194 #
193 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
194 # does not successfully save.
196 # does not successfully save.
195 def create_time_entry_activity_if_needed(activity)
197 def create_time_entry_activity_if_needed(activity)
196 if activity['parent_id']
198 if activity['parent_id']
197
199
198 parent_activity = TimeEntryActivity.find(activity['parent_id'])
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
199 activity['name'] = parent_activity.name
201 activity['name'] = parent_activity.name
200 activity['position'] = parent_activity.position
202 activity['position'] = parent_activity.position
201
203
202 if Enumeration.overridding_change?(activity, parent_activity)
204 if Enumeration.overridding_change?(activity, parent_activity)
203 project_activity = self.time_entry_activities.create(activity)
205 project_activity = self.time_entry_activities.create(activity)
204
206
205 if project_activity.new_record?
207 if project_activity.new_record?
206 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
207 else
209 else
208 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
209 end
211 end
210 end
212 end
211 end
213 end
212 end
214 end
213
215
214 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
215 #
217 #
216 # Examples:
218 # Examples:
217 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
218 # project.project_condition(false) => "projects.id = 1"
220 # project.project_condition(false) => "projects.id = 1"
219 def project_condition(with_subprojects)
221 def project_condition(with_subprojects)
220 cond = "#{Project.table_name}.id = #{id}"
222 cond = "#{Project.table_name}.id = #{id}"
221 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
222 cond
224 cond
223 end
225 end
224
226
225 def self.find(*args)
227 def self.find(*args)
226 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
227 project = find_by_identifier(*args)
229 project = find_by_identifier(*args)
228 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
229 project
231 project
230 else
232 else
231 super
233 super
232 end
234 end
233 end
235 end
234
236
235 def to_param
237 def to_param
236 # id is used for projects with a numeric identifier (compatibility)
238 # id is used for projects with a numeric identifier (compatibility)
237 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
238 end
240 end
239
241
240 def active?
242 def active?
241 self.status == STATUS_ACTIVE
243 self.status == STATUS_ACTIVE
242 end
244 end
243
245
244 def archived?
246 def archived?
245 self.status == STATUS_ARCHIVED
247 self.status == STATUS_ARCHIVED
246 end
248 end
247
249
248 # Archives the project and its descendants
250 # Archives the project and its descendants
249 def archive
251 def archive
250 # Check that there is no issue of a non descendant project that is assigned
252 # Check that there is no issue of a non descendant project that is assigned
251 # to one of the project or descendant versions
253 # to one of the project or descendant versions
252 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
253 if v_ids.any? && Issue.find(:first, :include => :project,
255 if v_ids.any? && Issue.find(:first, :include => :project,
254 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
255 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
256 return false
258 return false
257 end
259 end
258 Project.transaction do
260 Project.transaction do
259 archive!
261 archive!
260 end
262 end
261 true
263 true
262 end
264 end
263
265
264 # Unarchives the project
266 # Unarchives the project
265 # All its ancestors must be active
267 # All its ancestors must be active
266 def unarchive
268 def unarchive
267 return false if ancestors.detect {|a| !a.active?}
269 return false if ancestors.detect {|a| !a.active?}
268 update_attribute :status, STATUS_ACTIVE
270 update_attribute :status, STATUS_ACTIVE
269 end
271 end
270
272
271 # Returns an array of projects the project can be moved to
273 # Returns an array of projects the project can be moved to
272 # by the current user
274 # by the current user
273 def allowed_parents
275 def allowed_parents
274 return @allowed_parents if @allowed_parents
276 return @allowed_parents if @allowed_parents
275 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
276 @allowed_parents = @allowed_parents - self_and_descendants
278 @allowed_parents = @allowed_parents - self_and_descendants
277 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
278 @allowed_parents << nil
280 @allowed_parents << nil
279 end
281 end
280 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
281 @allowed_parents << parent
283 @allowed_parents << parent
282 end
284 end
283 @allowed_parents
285 @allowed_parents
284 end
286 end
285
287
286 # Sets the parent of the project with authorization check
288 # Sets the parent of the project with authorization check
287 def set_allowed_parent!(p)
289 def set_allowed_parent!(p)
288 unless p.nil? || p.is_a?(Project)
290 unless p.nil? || p.is_a?(Project)
289 if p.to_s.blank?
291 if p.to_s.blank?
290 p = nil
292 p = nil
291 else
293 else
292 p = Project.find_by_id(p)
294 p = Project.find_by_id(p)
293 return false unless p
295 return false unless p
294 end
296 end
295 end
297 end
296 if p.nil?
298 if p.nil?
297 if !new_record? && allowed_parents.empty?
299 if !new_record? && allowed_parents.empty?
298 return false
300 return false
299 end
301 end
300 elsif !allowed_parents.include?(p)
302 elsif !allowed_parents.include?(p)
301 return false
303 return false
302 end
304 end
303 set_parent!(p)
305 set_parent!(p)
304 end
306 end
305
307
306 # Sets the parent of the project
308 # Sets the parent of the project
307 # Argument can be either a Project, a String, a Fixnum or nil
309 # Argument can be either a Project, a String, a Fixnum or nil
308 def set_parent!(p)
310 def set_parent!(p)
309 unless p.nil? || p.is_a?(Project)
311 unless p.nil? || p.is_a?(Project)
310 if p.to_s.blank?
312 if p.to_s.blank?
311 p = nil
313 p = nil
312 else
314 else
313 p = Project.find_by_id(p)
315 p = Project.find_by_id(p)
314 return false unless p
316 return false unless p
315 end
317 end
316 end
318 end
317 if p == parent && !p.nil?
319 if p == parent && !p.nil?
318 # Nothing to do
320 # Nothing to do
319 true
321 true
320 elsif p.nil? || (p.active? && move_possible?(p))
322 elsif p.nil? || (p.active? && move_possible?(p))
321 # Insert the project so that target's children or root projects stay alphabetically sorted
323 # Insert the project so that target's children or root projects stay alphabetically sorted
322 sibs = (p.nil? ? self.class.roots : p.children)
324 sibs = (p.nil? ? self.class.roots : p.children)
323 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
324 if to_be_inserted_before
326 if to_be_inserted_before
325 move_to_left_of(to_be_inserted_before)
327 move_to_left_of(to_be_inserted_before)
326 elsif p.nil?
328 elsif p.nil?
327 if sibs.empty?
329 if sibs.empty?
328 # move_to_root adds the project in first (ie. left) position
330 # move_to_root adds the project in first (ie. left) position
329 move_to_root
331 move_to_root
330 else
332 else
331 move_to_right_of(sibs.last) unless self == sibs.last
333 move_to_right_of(sibs.last) unless self == sibs.last
332 end
334 end
333 else
335 else
334 # move_to_child_of adds the project in last (ie.right) position
336 # move_to_child_of adds the project in last (ie.right) position
335 move_to_child_of(p)
337 move_to_child_of(p)
336 end
338 end
337 Issue.update_versions_from_hierarchy_change(self)
339 Issue.update_versions_from_hierarchy_change(self)
338 true
340 true
339 else
341 else
340 # Can not move to the given target
342 # Can not move to the given target
341 false
343 false
342 end
344 end
343 end
345 end
344
346
345 # Returns an array of the trackers used by the project and its active sub projects
347 # Returns an array of the trackers used by the project and its active sub projects
346 def rolled_up_trackers
348 def rolled_up_trackers
347 @rolled_up_trackers ||=
349 @rolled_up_trackers ||=
348 Tracker.find(:all, :include => :projects,
350 Tracker.find(:all, :include => :projects,
349 :select => "DISTINCT #{Tracker.table_name}.*",
351 :select => "DISTINCT #{Tracker.table_name}.*",
350 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
351 :order => "#{Tracker.table_name}.position")
353 :order => "#{Tracker.table_name}.position")
352 end
354 end
353
355
354 # Closes open and locked project versions that are completed
356 # Closes open and locked project versions that are completed
355 def close_completed_versions
357 def close_completed_versions
356 Version.transaction do
358 Version.transaction do
357 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
358 if version.completed?
360 if version.completed?
359 version.update_attribute(:status, 'closed')
361 version.update_attribute(:status, 'closed')
360 end
362 end
361 end
363 end
362 end
364 end
363 end
365 end
364
366
365 # Returns a scope of the Versions on subprojects
367 # Returns a scope of the Versions on subprojects
366 def rolled_up_versions
368 def rolled_up_versions
367 @rolled_up_versions ||=
369 @rolled_up_versions ||=
368 Version.scoped(:include => :project,
370 Version.scoped(:include => :project,
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
370 end
372 end
371
373
372 # Returns a scope of the Versions used by the project
374 # Returns a scope of the Versions used by the project
373 def shared_versions
375 def shared_versions
374 @shared_versions ||=
376 @shared_versions ||=
375 Version.scoped(:include => :project,
377 Version.scoped(:include => :project,
376 :conditions => "#{Project.table_name}.id = #{id}" +
378 :conditions => "#{Project.table_name}.id = #{id}" +
377 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
378 " #{Version.table_name}.sharing = 'system'" +
380 " #{Version.table_name}.sharing = 'system'" +
379 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
380 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
381 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
382 "))")
384 "))")
383 end
385 end
384
386
385 # Returns a hash of project users grouped by role
387 # Returns a hash of project users grouped by role
386 def users_by_role
388 def users_by_role
387 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
388 m.roles.each do |r|
390 m.roles.each do |r|
389 h[r] ||= []
391 h[r] ||= []
390 h[r] << m.user
392 h[r] << m.user
391 end
393 end
392 h
394 h
393 end
395 end
394 end
396 end
395
397
396 # Deletes all project's members
398 # Deletes all project's members
397 def delete_all_members
399 def delete_all_members
398 me, mr = Member.table_name, MemberRole.table_name
400 me, mr = Member.table_name, MemberRole.table_name
399 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
400 Member.delete_all(['project_id = ?', id])
402 Member.delete_all(['project_id = ?', id])
401 end
403 end
402
404
403 # Users issues can be assigned to
405 # Users issues can be assigned to
404 def assignable_users
406 def assignable_users
405 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
406 end
408 end
407
409
408 # Returns the mail adresses of users that should be always notified on project events
410 # Returns the mail adresses of users that should be always notified on project events
409 def recipients
411 def recipients
410 notified_users.collect {|user| user.mail}
412 notified_users.collect {|user| user.mail}
411 end
413 end
412
414
413 # Returns the users that should be notified on project events
415 # Returns the users that should be notified on project events
414 def notified_users
416 def notified_users
415 # TODO: User part should be extracted to User#notify_about?
417 # TODO: User part should be extracted to User#notify_about?
416 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
417 end
419 end
418
420
419 # Returns an array of all custom fields enabled for project issues
421 # Returns an array of all custom fields enabled for project issues
420 # (explictly associated custom fields and custom fields enabled for all projects)
422 # (explictly associated custom fields and custom fields enabled for all projects)
421 def all_issue_custom_fields
423 def all_issue_custom_fields
422 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
423 end
425 end
424
426
425 def project
427 def project
426 self
428 self
427 end
429 end
428
430
429 def <=>(project)
431 def <=>(project)
430 name.downcase <=> project.name.downcase
432 name.downcase <=> project.name.downcase
431 end
433 end
432
434
433 def to_s
435 def to_s
434 name
436 name
435 end
437 end
436
438
437 # Returns a short description of the projects (first lines)
439 # Returns a short description of the projects (first lines)
438 def short_description(length = 255)
440 def short_description(length = 255)
439 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
440 end
442 end
441
443
442 def css_classes
444 def css_classes
443 s = 'project'
445 s = 'project'
444 s << ' root' if root?
446 s << ' root' if root?
445 s << ' child' if child?
447 s << ' child' if child?
446 s << (leaf? ? ' leaf' : ' parent')
448 s << (leaf? ? ' leaf' : ' parent')
447 s
449 s
448 end
450 end
449
451
450 # The earliest start date of a project, based on it's issues and versions
452 # The earliest start date of a project, based on it's issues and versions
451 def start_date
453 def start_date
452 [
454 [
453 issues.minimum('start_date'),
455 issues.minimum('start_date'),
454 shared_versions.collect(&:effective_date),
456 shared_versions.collect(&:effective_date),
455 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
457 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
456 ].flatten.compact.min
458 ].flatten.compact.min
457 end
459 end
458
460
459 # The latest due date of an issue or version
461 # The latest due date of an issue or version
460 def due_date
462 def due_date
461 [
463 [
462 issues.maximum('due_date'),
464 issues.maximum('due_date'),
463 shared_versions.collect(&:effective_date),
465 shared_versions.collect(&:effective_date),
464 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
465 ].flatten.compact.max
467 ].flatten.compact.max
466 end
468 end
467
469
468 def overdue?
470 def overdue?
469 active? && !due_date.nil? && (due_date < Date.today)
471 active? && !due_date.nil? && (due_date < Date.today)
470 end
472 end
471
473
472 # Returns the percent completed for this project, based on the
474 # Returns the percent completed for this project, based on the
473 # progress on it's versions.
475 # progress on it's versions.
474 def completed_percent(options={:include_subprojects => false})
476 def completed_percent(options={:include_subprojects => false})
475 if options.delete(:include_subprojects)
477 if options.delete(:include_subprojects)
476 total = self_and_descendants.collect(&:completed_percent).sum
478 total = self_and_descendants.collect(&:completed_percent).sum
477
479
478 total / self_and_descendants.count
480 total / self_and_descendants.count
479 else
481 else
480 if versions.count > 0
482 if versions.count > 0
481 total = versions.collect(&:completed_pourcent).sum
483 total = versions.collect(&:completed_pourcent).sum
482
484
483 total / versions.count
485 total / versions.count
484 else
486 else
485 100
487 100
486 end
488 end
487 end
489 end
488 end
490 end
489
491
490 # Return true if this project is allowed to do the specified action.
492 # Return true if this project is allowed to do the specified action.
491 # action can be:
493 # action can be:
492 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
493 # * a permission Symbol (eg. :edit_project)
495 # * a permission Symbol (eg. :edit_project)
494 def allows_to?(action)
496 def allows_to?(action)
495 if action.is_a? Hash
497 if action.is_a? Hash
496 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
497 else
499 else
498 allowed_permissions.include? action
500 allowed_permissions.include? action
499 end
501 end
500 end
502 end
501
503
502 def module_enabled?(module_name)
504 def module_enabled?(module_name)
503 module_name = module_name.to_s
505 module_name = module_name.to_s
504 enabled_modules.detect {|m| m.name == module_name}
506 enabled_modules.detect {|m| m.name == module_name}
505 end
507 end
506
508
507 def enabled_module_names=(module_names)
509 def enabled_module_names=(module_names)
508 if module_names && module_names.is_a?(Array)
510 if module_names && module_names.is_a?(Array)
509 module_names = module_names.collect(&:to_s).reject(&:blank?)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
510 # remove disabled modules
512 # remove disabled modules
511 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
512 # add new modules
514 # add new modules
513 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
514 else
516 else
515 enabled_modules.clear
517 enabled_modules.clear
516 end
518 end
517 end
519 end
518
520
519 # Returns an array of the enabled modules names
521 # Returns an array of the enabled modules names
520 def enabled_module_names
522 def enabled_module_names
521 enabled_modules.collect(&:name)
523 enabled_modules.collect(&:name)
522 end
524 end
525
526 safe_attributes 'name',
527 'description',
528 'homepage',
529 'is_public',
530 'identifier',
531 'custom_field_values',
532 'custom_fields',
533 'tracker_ids'
523
534
524 # Returns an array of projects that are in this project's hierarchy
535 # Returns an array of projects that are in this project's hierarchy
525 #
536 #
526 # Example: parents, children, siblings
537 # Example: parents, children, siblings
527 def hierarchy
538 def hierarchy
528 parents = project.self_and_ancestors || []
539 parents = project.self_and_ancestors || []
529 descendants = project.descendants || []
540 descendants = project.descendants || []
530 project_hierarchy = parents | descendants # Set union
541 project_hierarchy = parents | descendants # Set union
531 end
542 end
532
543
533 # Returns an auto-generated project identifier based on the last identifier used
544 # Returns an auto-generated project identifier based on the last identifier used
534 def self.next_identifier
545 def self.next_identifier
535 p = Project.find(:first, :order => 'created_on DESC')
546 p = Project.find(:first, :order => 'created_on DESC')
536 p.nil? ? nil : p.identifier.to_s.succ
547 p.nil? ? nil : p.identifier.to_s.succ
537 end
548 end
538
549
539 # Copies and saves the Project instance based on the +project+.
550 # Copies and saves the Project instance based on the +project+.
540 # Duplicates the source project's:
551 # Duplicates the source project's:
541 # * Wiki
552 # * Wiki
542 # * Versions
553 # * Versions
543 # * Categories
554 # * Categories
544 # * Issues
555 # * Issues
545 # * Members
556 # * Members
546 # * Queries
557 # * Queries
547 #
558 #
548 # Accepts an +options+ argument to specify what to copy
559 # Accepts an +options+ argument to specify what to copy
549 #
560 #
550 # Examples:
561 # Examples:
551 # project.copy(1) # => copies everything
562 # project.copy(1) # => copies everything
552 # project.copy(1, :only => 'members') # => copies members only
563 # project.copy(1, :only => 'members') # => copies members only
553 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
564 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
554 def copy(project, options={})
565 def copy(project, options={})
555 project = project.is_a?(Project) ? project : Project.find(project)
566 project = project.is_a?(Project) ? project : Project.find(project)
556
567
557 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
568 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
558 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
569 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
559
570
560 Project.transaction do
571 Project.transaction do
561 if save
572 if save
562 reload
573 reload
563 to_be_copied.each do |name|
574 to_be_copied.each do |name|
564 send "copy_#{name}", project
575 send "copy_#{name}", project
565 end
576 end
566 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
577 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
567 save
578 save
568 end
579 end
569 end
580 end
570 end
581 end
571
582
572
583
573 # Copies +project+ and returns the new instance. This will not save
584 # Copies +project+ and returns the new instance. This will not save
574 # the copy
585 # the copy
575 def self.copy_from(project)
586 def self.copy_from(project)
576 begin
587 begin
577 project = project.is_a?(Project) ? project : Project.find(project)
588 project = project.is_a?(Project) ? project : Project.find(project)
578 if project
589 if project
579 # clear unique attributes
590 # clear unique attributes
580 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
591 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
581 copy = Project.new(attributes)
592 copy = Project.new(attributes)
582 copy.enabled_modules = project.enabled_modules
593 copy.enabled_modules = project.enabled_modules
583 copy.trackers = project.trackers
594 copy.trackers = project.trackers
584 copy.custom_values = project.custom_values.collect {|v| v.clone}
595 copy.custom_values = project.custom_values.collect {|v| v.clone}
585 copy.issue_custom_fields = project.issue_custom_fields
596 copy.issue_custom_fields = project.issue_custom_fields
586 return copy
597 return copy
587 else
598 else
588 return nil
599 return nil
589 end
600 end
590 rescue ActiveRecord::RecordNotFound
601 rescue ActiveRecord::RecordNotFound
591 return nil
602 return nil
592 end
603 end
593 end
604 end
594
605
595 # Yields the given block for each project with its level in the tree
606 # Yields the given block for each project with its level in the tree
596 def self.project_tree(projects, &block)
607 def self.project_tree(projects, &block)
597 ancestors = []
608 ancestors = []
598 projects.sort_by(&:lft).each do |project|
609 projects.sort_by(&:lft).each do |project|
599 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
610 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
600 ancestors.pop
611 ancestors.pop
601 end
612 end
602 yield project, ancestors.size
613 yield project, ancestors.size
603 ancestors << project
614 ancestors << project
604 end
615 end
605 end
616 end
606
617
607 private
618 private
608
619
609 # Destroys children before destroying self
620 # Destroys children before destroying self
610 def destroy_children
621 def destroy_children
611 children.each do |child|
622 children.each do |child|
612 child.destroy
623 child.destroy
613 end
624 end
614 end
625 end
615
626
616 # Copies wiki from +project+
627 # Copies wiki from +project+
617 def copy_wiki(project)
628 def copy_wiki(project)
618 # Check that the source project has a wiki first
629 # Check that the source project has a wiki first
619 unless project.wiki.nil?
630 unless project.wiki.nil?
620 self.wiki ||= Wiki.new
631 self.wiki ||= Wiki.new
621 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
632 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
622 wiki_pages_map = {}
633 wiki_pages_map = {}
623 project.wiki.pages.each do |page|
634 project.wiki.pages.each do |page|
624 # Skip pages without content
635 # Skip pages without content
625 next if page.content.nil?
636 next if page.content.nil?
626 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
637 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
627 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
638 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
628 new_wiki_page.content = new_wiki_content
639 new_wiki_page.content = new_wiki_content
629 wiki.pages << new_wiki_page
640 wiki.pages << new_wiki_page
630 wiki_pages_map[page.id] = new_wiki_page
641 wiki_pages_map[page.id] = new_wiki_page
631 end
642 end
632 wiki.save
643 wiki.save
633 # Reproduce page hierarchy
644 # Reproduce page hierarchy
634 project.wiki.pages.each do |page|
645 project.wiki.pages.each do |page|
635 if page.parent_id && wiki_pages_map[page.id]
646 if page.parent_id && wiki_pages_map[page.id]
636 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
647 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
637 wiki_pages_map[page.id].save
648 wiki_pages_map[page.id].save
638 end
649 end
639 end
650 end
640 end
651 end
641 end
652 end
642
653
643 # Copies versions from +project+
654 # Copies versions from +project+
644 def copy_versions(project)
655 def copy_versions(project)
645 project.versions.each do |version|
656 project.versions.each do |version|
646 new_version = Version.new
657 new_version = Version.new
647 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
658 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
648 self.versions << new_version
659 self.versions << new_version
649 end
660 end
650 end
661 end
651
662
652 # Copies issue categories from +project+
663 # Copies issue categories from +project+
653 def copy_issue_categories(project)
664 def copy_issue_categories(project)
654 project.issue_categories.each do |issue_category|
665 project.issue_categories.each do |issue_category|
655 new_issue_category = IssueCategory.new
666 new_issue_category = IssueCategory.new
656 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
667 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
657 self.issue_categories << new_issue_category
668 self.issue_categories << new_issue_category
658 end
669 end
659 end
670 end
660
671
661 # Copies issues from +project+
672 # Copies issues from +project+
662 def copy_issues(project)
673 def copy_issues(project)
663 # Stores the source issue id as a key and the copied issues as the
674 # Stores the source issue id as a key and the copied issues as the
664 # value. Used to map the two togeather for issue relations.
675 # value. Used to map the two togeather for issue relations.
665 issues_map = {}
676 issues_map = {}
666
677
667 # Get issues sorted by root_id, lft so that parent issues
678 # Get issues sorted by root_id, lft so that parent issues
668 # get copied before their children
679 # get copied before their children
669 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
680 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
670 new_issue = Issue.new
681 new_issue = Issue.new
671 new_issue.copy_from(issue)
682 new_issue.copy_from(issue)
672 new_issue.project = self
683 new_issue.project = self
673 # Reassign fixed_versions by name, since names are unique per
684 # Reassign fixed_versions by name, since names are unique per
674 # project and the versions for self are not yet saved
685 # project and the versions for self are not yet saved
675 if issue.fixed_version
686 if issue.fixed_version
676 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
687 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
677 end
688 end
678 # Reassign the category by name, since names are unique per
689 # Reassign the category by name, since names are unique per
679 # project and the categories for self are not yet saved
690 # project and the categories for self are not yet saved
680 if issue.category
691 if issue.category
681 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
692 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
682 end
693 end
683 # Parent issue
694 # Parent issue
684 if issue.parent_id
695 if issue.parent_id
685 if copied_parent = issues_map[issue.parent_id]
696 if copied_parent = issues_map[issue.parent_id]
686 new_issue.parent_issue_id = copied_parent.id
697 new_issue.parent_issue_id = copied_parent.id
687 end
698 end
688 end
699 end
689
700
690 self.issues << new_issue
701 self.issues << new_issue
691 if new_issue.new_record?
702 if new_issue.new_record?
692 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
703 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
693 else
704 else
694 issues_map[issue.id] = new_issue unless new_issue.new_record?
705 issues_map[issue.id] = new_issue unless new_issue.new_record?
695 end
706 end
696 end
707 end
697
708
698 # Relations after in case issues related each other
709 # Relations after in case issues related each other
699 project.issues.each do |issue|
710 project.issues.each do |issue|
700 new_issue = issues_map[issue.id]
711 new_issue = issues_map[issue.id]
701 unless new_issue
712 unless new_issue
702 # Issue was not copied
713 # Issue was not copied
703 next
714 next
704 end
715 end
705
716
706 # Relations
717 # Relations
707 issue.relations_from.each do |source_relation|
718 issue.relations_from.each do |source_relation|
708 new_issue_relation = IssueRelation.new
719 new_issue_relation = IssueRelation.new
709 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
720 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
710 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
721 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
711 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
722 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
712 new_issue_relation.issue_to = source_relation.issue_to
723 new_issue_relation.issue_to = source_relation.issue_to
713 end
724 end
714 new_issue.relations_from << new_issue_relation
725 new_issue.relations_from << new_issue_relation
715 end
726 end
716
727
717 issue.relations_to.each do |source_relation|
728 issue.relations_to.each do |source_relation|
718 new_issue_relation = IssueRelation.new
729 new_issue_relation = IssueRelation.new
719 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
730 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
720 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
731 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
721 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
732 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
722 new_issue_relation.issue_from = source_relation.issue_from
733 new_issue_relation.issue_from = source_relation.issue_from
723 end
734 end
724 new_issue.relations_to << new_issue_relation
735 new_issue.relations_to << new_issue_relation
725 end
736 end
726 end
737 end
727 end
738 end
728
739
729 # Copies members from +project+
740 # Copies members from +project+
730 def copy_members(project)
741 def copy_members(project)
731 project.memberships.each do |member|
742 project.memberships.each do |member|
732 new_member = Member.new
743 new_member = Member.new
733 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
744 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
734 # only copy non inherited roles
745 # only copy non inherited roles
735 # inherited roles will be added when copying the group membership
746 # inherited roles will be added when copying the group membership
736 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
747 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
737 next if role_ids.empty?
748 next if role_ids.empty?
738 new_member.role_ids = role_ids
749 new_member.role_ids = role_ids
739 new_member.project = self
750 new_member.project = self
740 self.members << new_member
751 self.members << new_member
741 end
752 end
742 end
753 end
743
754
744 # Copies queries from +project+
755 # Copies queries from +project+
745 def copy_queries(project)
756 def copy_queries(project)
746 project.queries.each do |query|
757 project.queries.each do |query|
747 new_query = Query.new
758 new_query = Query.new
748 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
759 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
749 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
760 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
750 new_query.project = self
761 new_query.project = self
751 self.queries << new_query
762 self.queries << new_query
752 end
763 end
753 end
764 end
754
765
755 # Copies boards from +project+
766 # Copies boards from +project+
756 def copy_boards(project)
767 def copy_boards(project)
757 project.boards.each do |board|
768 project.boards.each do |board|
758 new_board = Board.new
769 new_board = Board.new
759 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
770 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
760 new_board.project = self
771 new_board.project = self
761 self.boards << new_board
772 self.boards << new_board
762 end
773 end
763 end
774 end
764
775
765 def allowed_permissions
776 def allowed_permissions
766 @allowed_permissions ||= begin
777 @allowed_permissions ||= begin
767 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
778 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
768 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
779 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
769 end
780 end
770 end
781 end
771
782
772 def allowed_actions
783 def allowed_actions
773 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
784 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
774 end
785 end
775
786
776 # Returns all the active Systemwide and project specific activities
787 # Returns all the active Systemwide and project specific activities
777 def active_activities
788 def active_activities
778 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
789 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
779
790
780 if overridden_activity_ids.empty?
791 if overridden_activity_ids.empty?
781 return TimeEntryActivity.shared.active
792 return TimeEntryActivity.shared.active
782 else
793 else
783 return system_activities_and_project_overrides
794 return system_activities_and_project_overrides
784 end
795 end
785 end
796 end
786
797
787 # Returns all the Systemwide and project specific activities
798 # Returns all the Systemwide and project specific activities
788 # (inactive and active)
799 # (inactive and active)
789 def all_activities
800 def all_activities
790 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
801 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
791
802
792 if overridden_activity_ids.empty?
803 if overridden_activity_ids.empty?
793 return TimeEntryActivity.shared
804 return TimeEntryActivity.shared
794 else
805 else
795 return system_activities_and_project_overrides(true)
806 return system_activities_and_project_overrides(true)
796 end
807 end
797 end
808 end
798
809
799 # Returns the systemwide active activities merged with the project specific overrides
810 # Returns the systemwide active activities merged with the project specific overrides
800 def system_activities_and_project_overrides(include_inactive=false)
811 def system_activities_and_project_overrides(include_inactive=false)
801 if include_inactive
812 if include_inactive
802 return TimeEntryActivity.shared.
813 return TimeEntryActivity.shared.
803 find(:all,
814 find(:all,
804 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
815 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
805 self.time_entry_activities
816 self.time_entry_activities
806 else
817 else
807 return TimeEntryActivity.shared.active.
818 return TimeEntryActivity.shared.active.
808 find(:all,
819 find(:all,
809 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
820 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
810 self.time_entry_activities.active
821 self.time_entry_activities.active
811 end
822 end
812 end
823 end
813
824
814 # Archives subprojects recursively
825 # Archives subprojects recursively
815 def archive!
826 def archive!
816 children.each do |subproject|
827 children.each do |subproject|
817 subproject.send :archive!
828 subproject.send :archive!
818 end
829 end
819 update_attribute :status, STATUS_ARCHIVED
830 update_attribute :status, STATUS_ARCHIVED
820 end
831 end
821 end
832 end
@@ -1,483 +1,498
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21
21 include Redmine::SafeAttributes
22
22 # Account statuses
23 # Account statuses
23 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
24 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
25 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
26 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
27
28
28 USER_FORMATS = {
29 USER_FORMATS = {
29 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname => '#{firstname}',
31 :firstname => '#{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :username => '#{login}'
34 :username => '#{login}'
34 }
35 }
35
36
36 MAIL_NOTIFICATION_OPTIONS = [
37 MAIL_NOTIFICATION_OPTIONS = [
37 [:all, :label_user_mail_option_all],
38 [:all, :label_user_mail_option_all],
38 [:selected, :label_user_mail_option_selected],
39 [:selected, :label_user_mail_option_selected],
39 [:none, :label_user_mail_option_none],
40 [:none, :label_user_mail_option_none],
40 [:only_my_events, :label_user_mail_option_only_my_events],
41 [:only_my_events, :label_user_mail_option_only_my_events],
41 [:only_assigned, :label_user_mail_option_only_assigned],
42 [:only_assigned, :label_user_mail_option_only_assigned],
42 [:only_owner, :label_user_mail_option_only_owner]
43 [:only_owner, :label_user_mail_option_only_owner]
43 ]
44 ]
44
45
45 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
46 :after_remove => Proc.new {|user, group| group.user_removed(user)}
47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
47 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
48 has_many :changesets, :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
52 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
52 belongs_to :auth_source
53 belongs_to :auth_source
53
54
54 # Active non-anonymous users scope
55 # Active non-anonymous users scope
55 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56
57
57 acts_as_customizable
58 acts_as_customizable
58
59
59 attr_accessor :password, :password_confirmation
60 attr_accessor :password, :password_confirmation
60 attr_accessor :last_before_login_on
61 attr_accessor :last_before_login_on
61 # Prevents unauthorized assignments
62 # Prevents unauthorized assignments
62 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
63
64
64 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 # Login must contain lettres, numbers, underscores only
68 # Login must contain lettres, numbers, underscores only
68 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 validates_length_of :login, :maximum => 30
70 validates_length_of :login, :maximum => 30
70 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
71 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
71 validates_length_of :firstname, :lastname, :maximum => 30
72 validates_length_of :firstname, :lastname, :maximum => 30
72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 validates_confirmation_of :password, :allow_nil => true
75 validates_confirmation_of :password, :allow_nil => true
75
76
76 def before_create
77 def before_create
77 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
78 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
78 true
79 true
79 end
80 end
80
81
81 def before_save
82 def before_save
82 # update hashed_password if password was set
83 # update hashed_password if password was set
83 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
84 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
84 end
85 end
85
86
86 def reload(*args)
87 def reload(*args)
87 @name = nil
88 @name = nil
88 super
89 super
89 end
90 end
90
91
91 def mail=(arg)
92 def mail=(arg)
92 write_attribute(:mail, arg.to_s.strip)
93 write_attribute(:mail, arg.to_s.strip)
93 end
94 end
94
95
95 def identity_url=(url)
96 def identity_url=(url)
96 if url.blank?
97 if url.blank?
97 write_attribute(:identity_url, '')
98 write_attribute(:identity_url, '')
98 else
99 else
99 begin
100 begin
100 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
101 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
101 rescue OpenIdAuthentication::InvalidOpenId
102 rescue OpenIdAuthentication::InvalidOpenId
102 # Invlaid url, don't save
103 # Invlaid url, don't save
103 end
104 end
104 end
105 end
105 self.read_attribute(:identity_url)
106 self.read_attribute(:identity_url)
106 end
107 end
107
108
108 # Returns the user that matches provided login and password, or nil
109 # Returns the user that matches provided login and password, or nil
109 def self.try_to_login(login, password)
110 def self.try_to_login(login, password)
110 # Make sure no one can sign in with an empty password
111 # Make sure no one can sign in with an empty password
111 return nil if password.to_s.empty?
112 return nil if password.to_s.empty?
112 user = find_by_login(login)
113 user = find_by_login(login)
113 if user
114 if user
114 # user is already in local database
115 # user is already in local database
115 return nil if !user.active?
116 return nil if !user.active?
116 if user.auth_source
117 if user.auth_source
117 # user has an external authentication method
118 # user has an external authentication method
118 return nil unless user.auth_source.authenticate(login, password)
119 return nil unless user.auth_source.authenticate(login, password)
119 else
120 else
120 # authentication with local password
121 # authentication with local password
121 return nil unless User.hash_password(password) == user.hashed_password
122 return nil unless User.hash_password(password) == user.hashed_password
122 end
123 end
123 else
124 else
124 # user is not yet registered, try to authenticate with available sources
125 # user is not yet registered, try to authenticate with available sources
125 attrs = AuthSource.authenticate(login, password)
126 attrs = AuthSource.authenticate(login, password)
126 if attrs
127 if attrs
127 user = new(attrs)
128 user = new(attrs)
128 user.login = login
129 user.login = login
129 user.language = Setting.default_language
130 user.language = Setting.default_language
130 if user.save
131 if user.save
131 user.reload
132 user.reload
132 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
133 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
133 end
134 end
134 end
135 end
135 end
136 end
136 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
137 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
137 user
138 user
138 rescue => text
139 rescue => text
139 raise text
140 raise text
140 end
141 end
141
142
142 # Returns the user who matches the given autologin +key+ or nil
143 # Returns the user who matches the given autologin +key+ or nil
143 def self.try_to_autologin(key)
144 def self.try_to_autologin(key)
144 tokens = Token.find_all_by_action_and_value('autologin', key)
145 tokens = Token.find_all_by_action_and_value('autologin', key)
145 # Make sure there's only 1 token that matches the key
146 # Make sure there's only 1 token that matches the key
146 if tokens.size == 1
147 if tokens.size == 1
147 token = tokens.first
148 token = tokens.first
148 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
149 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
149 token.user.update_attribute(:last_login_on, Time.now)
150 token.user.update_attribute(:last_login_on, Time.now)
150 token.user
151 token.user
151 end
152 end
152 end
153 end
153 end
154 end
154
155
155 # Return user's full name for display
156 # Return user's full name for display
156 def name(formatter = nil)
157 def name(formatter = nil)
157 if formatter
158 if formatter
158 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
159 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
159 else
160 else
160 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
161 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
161 end
162 end
162 end
163 end
163
164
164 def active?
165 def active?
165 self.status == STATUS_ACTIVE
166 self.status == STATUS_ACTIVE
166 end
167 end
167
168
168 def registered?
169 def registered?
169 self.status == STATUS_REGISTERED
170 self.status == STATUS_REGISTERED
170 end
171 end
171
172
172 def locked?
173 def locked?
173 self.status == STATUS_LOCKED
174 self.status == STATUS_LOCKED
174 end
175 end
175
176
176 def activate
177 def activate
177 self.status = STATUS_ACTIVE
178 self.status = STATUS_ACTIVE
178 end
179 end
179
180
180 def register
181 def register
181 self.status = STATUS_REGISTERED
182 self.status = STATUS_REGISTERED
182 end
183 end
183
184
184 def lock
185 def lock
185 self.status = STATUS_LOCKED
186 self.status = STATUS_LOCKED
186 end
187 end
187
188
188 def activate!
189 def activate!
189 update_attribute(:status, STATUS_ACTIVE)
190 update_attribute(:status, STATUS_ACTIVE)
190 end
191 end
191
192
192 def register!
193 def register!
193 update_attribute(:status, STATUS_REGISTERED)
194 update_attribute(:status, STATUS_REGISTERED)
194 end
195 end
195
196
196 def lock!
197 def lock!
197 update_attribute(:status, STATUS_LOCKED)
198 update_attribute(:status, STATUS_LOCKED)
198 end
199 end
199
200
200 def check_password?(clear_password)
201 def check_password?(clear_password)
201 if auth_source_id.present?
202 if auth_source_id.present?
202 auth_source.authenticate(self.login, clear_password)
203 auth_source.authenticate(self.login, clear_password)
203 else
204 else
204 User.hash_password(clear_password) == self.hashed_password
205 User.hash_password(clear_password) == self.hashed_password
205 end
206 end
206 end
207 end
207
208
208 # Does the backend storage allow this user to change their password?
209 # Does the backend storage allow this user to change their password?
209 def change_password_allowed?
210 def change_password_allowed?
210 return true if auth_source_id.blank?
211 return true if auth_source_id.blank?
211 return auth_source.allow_password_changes?
212 return auth_source.allow_password_changes?
212 end
213 end
213
214
214 # Generate and set a random password. Useful for automated user creation
215 # Generate and set a random password. Useful for automated user creation
215 # Based on Token#generate_token_value
216 # Based on Token#generate_token_value
216 #
217 #
217 def random_password
218 def random_password
218 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
219 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
219 password = ''
220 password = ''
220 40.times { |i| password << chars[rand(chars.size-1)] }
221 40.times { |i| password << chars[rand(chars.size-1)] }
221 self.password = password
222 self.password = password
222 self.password_confirmation = password
223 self.password_confirmation = password
223 self
224 self
224 end
225 end
225
226
226 def pref
227 def pref
227 self.preference ||= UserPreference.new(:user => self)
228 self.preference ||= UserPreference.new(:user => self)
228 end
229 end
229
230
230 def time_zone
231 def time_zone
231 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
232 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
232 end
233 end
233
234
234 def wants_comments_in_reverse_order?
235 def wants_comments_in_reverse_order?
235 self.pref[:comments_sorting] == 'desc'
236 self.pref[:comments_sorting] == 'desc'
236 end
237 end
237
238
238 # Return user's RSS key (a 40 chars long string), used to access feeds
239 # Return user's RSS key (a 40 chars long string), used to access feeds
239 def rss_key
240 def rss_key
240 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
241 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
241 token.value
242 token.value
242 end
243 end
243
244
244 # Return user's API key (a 40 chars long string), used to access the API
245 # Return user's API key (a 40 chars long string), used to access the API
245 def api_key
246 def api_key
246 token = self.api_token || self.create_api_token(:action => 'api')
247 token = self.api_token || self.create_api_token(:action => 'api')
247 token.value
248 token.value
248 end
249 end
249
250
250 # Return an array of project ids for which the user has explicitly turned mail notifications on
251 # Return an array of project ids for which the user has explicitly turned mail notifications on
251 def notified_projects_ids
252 def notified_projects_ids
252 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
253 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
253 end
254 end
254
255
255 def notified_project_ids=(ids)
256 def notified_project_ids=(ids)
256 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
257 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
257 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
258 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
258 @notified_projects_ids = nil
259 @notified_projects_ids = nil
259 notified_projects_ids
260 notified_projects_ids
260 end
261 end
261
262
262 # Only users that belong to more than 1 project can select projects for which they are notified
263 # Only users that belong to more than 1 project can select projects for which they are notified
263 def valid_notification_options
264 def valid_notification_options
264 # Note that @user.membership.size would fail since AR ignores
265 # Note that @user.membership.size would fail since AR ignores
265 # :include association option when doing a count
266 # :include association option when doing a count
266 if memberships.length < 1
267 if memberships.length < 1
267 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected}
268 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected}
268 else
269 else
269 MAIL_NOTIFICATION_OPTIONS
270 MAIL_NOTIFICATION_OPTIONS
270 end
271 end
271 end
272 end
272
273
273 # Find a user account by matching the exact login and then a case-insensitive
274 # Find a user account by matching the exact login and then a case-insensitive
274 # version. Exact matches will be given priority.
275 # version. Exact matches will be given priority.
275 def self.find_by_login(login)
276 def self.find_by_login(login)
276 # force string comparison to be case sensitive on MySQL
277 # force string comparison to be case sensitive on MySQL
277 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
278 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
278
279
279 # First look for an exact match
280 # First look for an exact match
280 user = first(:conditions => ["#{type_cast} login = ?", login])
281 user = first(:conditions => ["#{type_cast} login = ?", login])
281 # Fail over to case-insensitive if none was found
282 # Fail over to case-insensitive if none was found
282 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
283 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
283 end
284 end
284
285
285 def self.find_by_rss_key(key)
286 def self.find_by_rss_key(key)
286 token = Token.find_by_value(key)
287 token = Token.find_by_value(key)
287 token && token.user.active? ? token.user : nil
288 token && token.user.active? ? token.user : nil
288 end
289 end
289
290
290 def self.find_by_api_key(key)
291 def self.find_by_api_key(key)
291 token = Token.find_by_action_and_value('api', key)
292 token = Token.find_by_action_and_value('api', key)
292 token && token.user.active? ? token.user : nil
293 token && token.user.active? ? token.user : nil
293 end
294 end
294
295
295 # Makes find_by_mail case-insensitive
296 # Makes find_by_mail case-insensitive
296 def self.find_by_mail(mail)
297 def self.find_by_mail(mail)
297 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
298 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
298 end
299 end
299
300
300 def to_s
301 def to_s
301 name
302 name
302 end
303 end
303
304
304 # Returns the current day according to user's time zone
305 # Returns the current day according to user's time zone
305 def today
306 def today
306 if time_zone.nil?
307 if time_zone.nil?
307 Date.today
308 Date.today
308 else
309 else
309 Time.now.in_time_zone(time_zone).to_date
310 Time.now.in_time_zone(time_zone).to_date
310 end
311 end
311 end
312 end
312
313
313 def logged?
314 def logged?
314 true
315 true
315 end
316 end
316
317
317 def anonymous?
318 def anonymous?
318 !logged?
319 !logged?
319 end
320 end
320
321
321 # Return user's roles for project
322 # Return user's roles for project
322 def roles_for_project(project)
323 def roles_for_project(project)
323 roles = []
324 roles = []
324 # No role on archived projects
325 # No role on archived projects
325 return roles unless project && project.active?
326 return roles unless project && project.active?
326 if logged?
327 if logged?
327 # Find project membership
328 # Find project membership
328 membership = memberships.detect {|m| m.project_id == project.id}
329 membership = memberships.detect {|m| m.project_id == project.id}
329 if membership
330 if membership
330 roles = membership.roles
331 roles = membership.roles
331 else
332 else
332 @role_non_member ||= Role.non_member
333 @role_non_member ||= Role.non_member
333 roles << @role_non_member
334 roles << @role_non_member
334 end
335 end
335 else
336 else
336 @role_anonymous ||= Role.anonymous
337 @role_anonymous ||= Role.anonymous
337 roles << @role_anonymous
338 roles << @role_anonymous
338 end
339 end
339 roles
340 roles
340 end
341 end
341
342
342 # Return true if the user is a member of project
343 # Return true if the user is a member of project
343 def member_of?(project)
344 def member_of?(project)
344 !roles_for_project(project).detect {|role| role.member?}.nil?
345 !roles_for_project(project).detect {|role| role.member?}.nil?
345 end
346 end
346
347
347 # Return true if the user is allowed to do the specified action on a specific context
348 # Return true if the user is allowed to do the specified action on a specific context
348 # Action can be:
349 # Action can be:
349 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 # * a permission Symbol (eg. :edit_project)
351 # * a permission Symbol (eg. :edit_project)
351 # Context can be:
352 # Context can be:
352 # * a project : returns true if user is allowed to do the specified action on this project
353 # * a project : returns true if user is allowed to do the specified action on this project
353 # * a group of projects : returns true if user is allowed on every project
354 # * a group of projects : returns true if user is allowed on every project
354 # * nil with options[:global] set : check if user has at least one role allowed for this action,
355 # * nil with options[:global] set : check if user has at least one role allowed for this action,
355 # or falls back to Non Member / Anonymous permissions depending if the user is logged
356 # or falls back to Non Member / Anonymous permissions depending if the user is logged
356 def allowed_to?(action, context, options={})
357 def allowed_to?(action, context, options={})
357 if context && context.is_a?(Project)
358 if context && context.is_a?(Project)
358 # No action allowed on archived projects
359 # No action allowed on archived projects
359 return false unless context.active?
360 return false unless context.active?
360 # No action allowed on disabled modules
361 # No action allowed on disabled modules
361 return false unless context.allows_to?(action)
362 return false unless context.allows_to?(action)
362 # Admin users are authorized for anything else
363 # Admin users are authorized for anything else
363 return true if admin?
364 return true if admin?
364
365
365 roles = roles_for_project(context)
366 roles = roles_for_project(context)
366 return false unless roles
367 return false unless roles
367 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
368 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
368
369
369 elsif context && context.is_a?(Array)
370 elsif context && context.is_a?(Array)
370 # Authorize if user is authorized on every element of the array
371 # Authorize if user is authorized on every element of the array
371 context.map do |project|
372 context.map do |project|
372 allowed_to?(action,project,options)
373 allowed_to?(action,project,options)
373 end.inject do |memo,allowed|
374 end.inject do |memo,allowed|
374 memo && allowed
375 memo && allowed
375 end
376 end
376 elsif options[:global]
377 elsif options[:global]
377 # Admin users are always authorized
378 # Admin users are always authorized
378 return true if admin?
379 return true if admin?
379
380
380 # authorize if user has at least one role that has this permission
381 # authorize if user has at least one role that has this permission
381 roles = memberships.collect {|m| m.roles}.flatten.uniq
382 roles = memberships.collect {|m| m.roles}.flatten.uniq
382 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
383 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
383 else
384 else
384 false
385 false
385 end
386 end
386 end
387 end
387
388
388 # Is the user allowed to do the specified action on any project?
389 # Is the user allowed to do the specified action on any project?
389 # See allowed_to? for the actions and valid options.
390 # See allowed_to? for the actions and valid options.
390 def allowed_to_globally?(action, options)
391 def allowed_to_globally?(action, options)
391 allowed_to?(action, nil, options.reverse_merge(:global => true))
392 allowed_to?(action, nil, options.reverse_merge(:global => true))
392 end
393 end
394
395 safe_attributes 'login',
396 'firstname',
397 'lastname',
398 'mail',
399 'mail_notification',
400 'language',
401 'custom_field_values',
402 'custom_fields',
403 'identity_url'
404
405 safe_attributes 'status',
406 'auth_source_id',
407 :if => lambda {|user, current_user| current_user.admin?}
393
408
394 # Utility method to help check if a user should be notified about an
409 # Utility method to help check if a user should be notified about an
395 # event.
410 # event.
396 #
411 #
397 # TODO: only supports Issue events currently
412 # TODO: only supports Issue events currently
398 def notify_about?(object)
413 def notify_about?(object)
399 case mail_notification.to_sym
414 case mail_notification.to_sym
400 when :all
415 when :all
401 true
416 true
402 when :selected
417 when :selected
403 # Handled by the Project
418 # Handled by the Project
404 when :none
419 when :none
405 false
420 false
406 when :only_my_events
421 when :only_my_events
407 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
422 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
408 true
423 true
409 else
424 else
410 false
425 false
411 end
426 end
412 when :only_assigned
427 when :only_assigned
413 if object.is_a?(Issue) && object.assigned_to == self
428 if object.is_a?(Issue) && object.assigned_to == self
414 true
429 true
415 else
430 else
416 false
431 false
417 end
432 end
418 when :only_owner
433 when :only_owner
419 if object.is_a?(Issue) && object.author == self
434 if object.is_a?(Issue) && object.author == self
420 true
435 true
421 else
436 else
422 false
437 false
423 end
438 end
424 else
439 else
425 false
440 false
426 end
441 end
427 end
442 end
428
443
429 def self.current=(user)
444 def self.current=(user)
430 @current_user = user
445 @current_user = user
431 end
446 end
432
447
433 def self.current
448 def self.current
434 @current_user ||= User.anonymous
449 @current_user ||= User.anonymous
435 end
450 end
436
451
437 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
452 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
438 # one anonymous user per database.
453 # one anonymous user per database.
439 def self.anonymous
454 def self.anonymous
440 anonymous_user = AnonymousUser.find(:first)
455 anonymous_user = AnonymousUser.find(:first)
441 if anonymous_user.nil?
456 if anonymous_user.nil?
442 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
457 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
443 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
458 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
444 end
459 end
445 anonymous_user
460 anonymous_user
446 end
461 end
447
462
448 protected
463 protected
449
464
450 def validate
465 def validate
451 # Password length validation based on setting
466 # Password length validation based on setting
452 if !password.nil? && password.size < Setting.password_min_length.to_i
467 if !password.nil? && password.size < Setting.password_min_length.to_i
453 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
468 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
454 end
469 end
455 end
470 end
456
471
457 private
472 private
458
473
459 # Return password digest
474 # Return password digest
460 def self.hash_password(clear_password)
475 def self.hash_password(clear_password)
461 Digest::SHA1.hexdigest(clear_password || "")
476 Digest::SHA1.hexdigest(clear_password || "")
462 end
477 end
463 end
478 end
464
479
465 class AnonymousUser < User
480 class AnonymousUser < User
466
481
467 def validate_on_create
482 def validate_on_create
468 # There should be only one AnonymousUser in the database
483 # There should be only one AnonymousUser in the database
469 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
484 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
470 end
485 end
471
486
472 def available_custom_fields
487 def available_custom_fields
473 []
488 []
474 end
489 end
475
490
476 # Overrides a few properties
491 # Overrides a few properties
477 def logged?; false end
492 def logged?; false end
478 def admin; false end
493 def admin; false end
479 def name(*args); I18n.t(:label_user_anonymous) end
494 def name(*args); I18n.t(:label_user_anonymous) end
480 def mail; nil end
495 def mail; nil end
481 def time_zone; nil end
496 def time_zone; nil end
482 def rss_key; nil end
497 def rss_key; nil end
483 end
498 end
@@ -1,460 +1,468
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values, :time_entries
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:projects)
41 assert_not_nil assigns(:projects)
42
42
43 assert_tag :ul, :child => {:tag => 'li',
43 assert_tag :ul, :child => {:tag => 'li',
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 :child => { :tag => 'ul',
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
47 :content => 'Child of private child'
48 }
48 }
49 }
49 }
50 }
50 }
51
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
52 assert_no_tag :a, :content => /Private child of eCookbook/
53 end
53 end
54
54
55 def test_index_atom
55 def test_index_atom
56 get :index, :format => 'atom'
56 get :index, :format => 'atom'
57 assert_response :success
57 assert_response :success
58 assert_template 'common/feed.atom.rxml'
58 assert_template 'common/feed.atom.rxml'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 end
61 end
62
62
63 context "#index" do
63 context "#index" do
64 context "by non-admin user with view_time_entries permission" do
64 context "by non-admin user with view_time_entries permission" do
65 setup do
65 setup do
66 @request.session[:user_id] = 3
66 @request.session[:user_id] = 3
67 end
67 end
68 should "show overall spent time link" do
68 should "show overall spent time link" do
69 get :index
69 get :index
70 assert_template 'index'
70 assert_template 'index'
71 assert_tag :a, :attributes => {:href => '/time_entries'}
71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 end
72 end
73 end
73 end
74
74
75 context "by non-admin user without view_time_entries permission" do
75 context "by non-admin user without view_time_entries permission" do
76 setup do
76 setup do
77 Role.find(2).remove_permission! :view_time_entries
77 Role.find(2).remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
80 @request.session[:user_id] = 3
80 @request.session[:user_id] = 3
81 end
81 end
82 should "not show overall spent time link" do
82 should "not show overall spent time link" do
83 get :index
83 get :index
84 assert_template 'index'
84 assert_template 'index'
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 context "#new" do
90 context "#new" do
91 context "by admin user" do
91 context "by admin user" do
92 setup do
92 setup do
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 end
94 end
95
95
96 should "accept get" do
96 should "accept get" do
97 get :new
97 get :new
98 assert_response :success
98 assert_response :success
99 assert_template 'new'
99 assert_template 'new'
100 end
100 end
101
101
102 end
102 end
103
103
104 context "by non-admin user with add_project permission" do
104 context "by non-admin user with add_project permission" do
105 setup do
105 setup do
106 Role.non_member.add_permission! :add_project
106 Role.non_member.add_permission! :add_project
107 @request.session[:user_id] = 9
107 @request.session[:user_id] = 9
108 end
108 end
109
109
110 should "accept get" do
110 should "accept get" do
111 get :new
111 get :new
112 assert_response :success
112 assert_response :success
113 assert_template 'new'
113 assert_template 'new'
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 end
115 end
116 end
116 end
117
117
118 context "by non-admin user with add_subprojects permission" do
118 context "by non-admin user with add_subprojects permission" do
119 setup do
119 setup do
120 Role.find(1).remove_permission! :add_project
120 Role.find(1).remove_permission! :add_project
121 Role.find(1).add_permission! :add_subprojects
121 Role.find(1).add_permission! :add_subprojects
122 @request.session[:user_id] = 2
122 @request.session[:user_id] = 2
123 end
123 end
124
124
125 should "accept get" do
125 should "accept get" do
126 get :new, :parent_id => 'ecookbook'
126 get :new, :parent_id => 'ecookbook'
127 assert_response :success
127 assert_response :success
128 assert_template 'new'
128 assert_template 'new'
129 # parent project selected
129 # parent project selected
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 # no empty value
132 # no empty value
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 :child => {:tag => 'option', :attributes => {:value => ''}}
134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 end
135 end
136 end
136 end
137
137
138 end
138 end
139
139
140 context "POST :create" do
140 context "POST :create" do
141 context "by admin user" do
141 context "by admin user" do
142 setup do
142 setup do
143 @request.session[:user_id] = 1
143 @request.session[:user_id] = 1
144 end
144 end
145
145
146 should "create a new project" do
146 should "create a new project" do
147 post :create, :project => { :name => "blog",
147 post :create,
148 :description => "weblog",
148 :project => {
149 :identifier => "blog",
149 :name => "blog",
150 :is_public => 1,
150 :description => "weblog",
151 :custom_field_values => { '3' => 'Beta' }
151 :homepage => 'http://weblog',
152 }
152 :identifier => "blog",
153 :is_public => 1,
154 :custom_field_values => { '3' => 'Beta' },
155 :tracker_ids => ['1', '3']
156 }
153 assert_redirected_to '/projects/blog/settings'
157 assert_redirected_to '/projects/blog/settings'
154
158
155 project = Project.find_by_name('blog')
159 project = Project.find_by_name('blog')
156 assert_kind_of Project, project
160 assert_kind_of Project, project
161 assert project.active?
157 assert_equal 'weblog', project.description
162 assert_equal 'weblog', project.description
163 assert_equal 'http://weblog', project.homepage
158 assert_equal true, project.is_public?
164 assert_equal true, project.is_public?
159 assert_nil project.parent
165 assert_nil project.parent
166 assert_equal 'Beta', project.custom_value_for(3).value
167 assert_equal [1, 3], project.trackers.map(&:id).sort
160 end
168 end
161
169
162 should "create a new subproject" do
170 should "create a new subproject" do
163 post :create, :project => { :name => "blog",
171 post :create, :project => { :name => "blog",
164 :description => "weblog",
172 :description => "weblog",
165 :identifier => "blog",
173 :identifier => "blog",
166 :is_public => 1,
174 :is_public => 1,
167 :custom_field_values => { '3' => 'Beta' },
175 :custom_field_values => { '3' => 'Beta' },
168 :parent_id => 1
176 :parent_id => 1
169 }
177 }
170 assert_redirected_to '/projects/blog/settings'
178 assert_redirected_to '/projects/blog/settings'
171
179
172 project = Project.find_by_name('blog')
180 project = Project.find_by_name('blog')
173 assert_kind_of Project, project
181 assert_kind_of Project, project
174 assert_equal Project.find(1), project.parent
182 assert_equal Project.find(1), project.parent
175 end
183 end
176 end
184 end
177
185
178 context "by non-admin user with add_project permission" do
186 context "by non-admin user with add_project permission" do
179 setup do
187 setup do
180 Role.non_member.add_permission! :add_project
188 Role.non_member.add_permission! :add_project
181 @request.session[:user_id] = 9
189 @request.session[:user_id] = 9
182 end
190 end
183
191
184 should "accept create a Project" do
192 should "accept create a Project" do
185 post :create, :project => { :name => "blog",
193 post :create, :project => { :name => "blog",
186 :description => "weblog",
194 :description => "weblog",
187 :identifier => "blog",
195 :identifier => "blog",
188 :is_public => 1,
196 :is_public => 1,
189 :custom_field_values => { '3' => 'Beta' }
197 :custom_field_values => { '3' => 'Beta' }
190 }
198 }
191
199
192 assert_redirected_to '/projects/blog/settings'
200 assert_redirected_to '/projects/blog/settings'
193
201
194 project = Project.find_by_name('blog')
202 project = Project.find_by_name('blog')
195 assert_kind_of Project, project
203 assert_kind_of Project, project
196 assert_equal 'weblog', project.description
204 assert_equal 'weblog', project.description
197 assert_equal true, project.is_public?
205 assert_equal true, project.is_public?
198
206
199 # User should be added as a project member
207 # User should be added as a project member
200 assert User.find(9).member_of?(project)
208 assert User.find(9).member_of?(project)
201 assert_equal 1, project.members.size
209 assert_equal 1, project.members.size
202 end
210 end
203
211
204 should "fail with parent_id" do
212 should "fail with parent_id" do
205 assert_no_difference 'Project.count' do
213 assert_no_difference 'Project.count' do
206 post :create, :project => { :name => "blog",
214 post :create, :project => { :name => "blog",
207 :description => "weblog",
215 :description => "weblog",
208 :identifier => "blog",
216 :identifier => "blog",
209 :is_public => 1,
217 :is_public => 1,
210 :custom_field_values => { '3' => 'Beta' },
218 :custom_field_values => { '3' => 'Beta' },
211 :parent_id => 1
219 :parent_id => 1
212 }
220 }
213 end
221 end
214 assert_response :success
222 assert_response :success
215 project = assigns(:project)
223 project = assigns(:project)
216 assert_kind_of Project, project
224 assert_kind_of Project, project
217 assert_not_nil project.errors.on(:parent_id)
225 assert_not_nil project.errors.on(:parent_id)
218 end
226 end
219 end
227 end
220
228
221 context "by non-admin user with add_subprojects permission" do
229 context "by non-admin user with add_subprojects permission" do
222 setup do
230 setup do
223 Role.find(1).remove_permission! :add_project
231 Role.find(1).remove_permission! :add_project
224 Role.find(1).add_permission! :add_subprojects
232 Role.find(1).add_permission! :add_subprojects
225 @request.session[:user_id] = 2
233 @request.session[:user_id] = 2
226 end
234 end
227
235
228 should "create a project with a parent_id" do
236 should "create a project with a parent_id" do
229 post :create, :project => { :name => "blog",
237 post :create, :project => { :name => "blog",
230 :description => "weblog",
238 :description => "weblog",
231 :identifier => "blog",
239 :identifier => "blog",
232 :is_public => 1,
240 :is_public => 1,
233 :custom_field_values => { '3' => 'Beta' },
241 :custom_field_values => { '3' => 'Beta' },
234 :parent_id => 1
242 :parent_id => 1
235 }
243 }
236 assert_redirected_to '/projects/blog/settings'
244 assert_redirected_to '/projects/blog/settings'
237 project = Project.find_by_name('blog')
245 project = Project.find_by_name('blog')
238 end
246 end
239
247
240 should "fail without parent_id" do
248 should "fail without parent_id" do
241 assert_no_difference 'Project.count' do
249 assert_no_difference 'Project.count' do
242 post :create, :project => { :name => "blog",
250 post :create, :project => { :name => "blog",
243 :description => "weblog",
251 :description => "weblog",
244 :identifier => "blog",
252 :identifier => "blog",
245 :is_public => 1,
253 :is_public => 1,
246 :custom_field_values => { '3' => 'Beta' }
254 :custom_field_values => { '3' => 'Beta' }
247 }
255 }
248 end
256 end
249 assert_response :success
257 assert_response :success
250 project = assigns(:project)
258 project = assigns(:project)
251 assert_kind_of Project, project
259 assert_kind_of Project, project
252 assert_not_nil project.errors.on(:parent_id)
260 assert_not_nil project.errors.on(:parent_id)
253 end
261 end
254
262
255 should "fail with unauthorized parent_id" do
263 should "fail with unauthorized parent_id" do
256 assert !User.find(2).member_of?(Project.find(6))
264 assert !User.find(2).member_of?(Project.find(6))
257 assert_no_difference 'Project.count' do
265 assert_no_difference 'Project.count' do
258 post :create, :project => { :name => "blog",
266 post :create, :project => { :name => "blog",
259 :description => "weblog",
267 :description => "weblog",
260 :identifier => "blog",
268 :identifier => "blog",
261 :is_public => 1,
269 :is_public => 1,
262 :custom_field_values => { '3' => 'Beta' },
270 :custom_field_values => { '3' => 'Beta' },
263 :parent_id => 6
271 :parent_id => 6
264 }
272 }
265 end
273 end
266 assert_response :success
274 assert_response :success
267 project = assigns(:project)
275 project = assigns(:project)
268 assert_kind_of Project, project
276 assert_kind_of Project, project
269 assert_not_nil project.errors.on(:parent_id)
277 assert_not_nil project.errors.on(:parent_id)
270 end
278 end
271 end
279 end
272 end
280 end
273
281
274 def test_show_by_id
282 def test_show_by_id
275 get :show, :id => 1
283 get :show, :id => 1
276 assert_response :success
284 assert_response :success
277 assert_template 'show'
285 assert_template 'show'
278 assert_not_nil assigns(:project)
286 assert_not_nil assigns(:project)
279 end
287 end
280
288
281 def test_show_by_identifier
289 def test_show_by_identifier
282 get :show, :id => 'ecookbook'
290 get :show, :id => 'ecookbook'
283 assert_response :success
291 assert_response :success
284 assert_template 'show'
292 assert_template 'show'
285 assert_not_nil assigns(:project)
293 assert_not_nil assigns(:project)
286 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
294 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
287
295
288 assert_tag 'li', :content => /Development status/
296 assert_tag 'li', :content => /Development status/
289 end
297 end
290
298
291 def test_show_should_not_display_hidden_custom_fields
299 def test_show_should_not_display_hidden_custom_fields
292 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
300 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
293 get :show, :id => 'ecookbook'
301 get :show, :id => 'ecookbook'
294 assert_response :success
302 assert_response :success
295 assert_template 'show'
303 assert_template 'show'
296 assert_not_nil assigns(:project)
304 assert_not_nil assigns(:project)
297
305
298 assert_no_tag 'li', :content => /Development status/
306 assert_no_tag 'li', :content => /Development status/
299 end
307 end
300
308
301 def test_show_should_not_fail_when_custom_values_are_nil
309 def test_show_should_not_fail_when_custom_values_are_nil
302 project = Project.find_by_identifier('ecookbook')
310 project = Project.find_by_identifier('ecookbook')
303 project.custom_values.first.update_attribute(:value, nil)
311 project.custom_values.first.update_attribute(:value, nil)
304 get :show, :id => 'ecookbook'
312 get :show, :id => 'ecookbook'
305 assert_response :success
313 assert_response :success
306 assert_template 'show'
314 assert_template 'show'
307 assert_not_nil assigns(:project)
315 assert_not_nil assigns(:project)
308 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
316 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
309 end
317 end
310
318
311 def show_archived_project_should_be_denied
319 def show_archived_project_should_be_denied
312 project = Project.find_by_identifier('ecookbook')
320 project = Project.find_by_identifier('ecookbook')
313 project.archive!
321 project.archive!
314
322
315 get :show, :id => 'ecookbook'
323 get :show, :id => 'ecookbook'
316 assert_response 403
324 assert_response 403
317 assert_nil assigns(:project)
325 assert_nil assigns(:project)
318 assert_tag :tag => 'p', :content => /archived/
326 assert_tag :tag => 'p', :content => /archived/
319 end
327 end
320
328
321 def test_private_subprojects_hidden
329 def test_private_subprojects_hidden
322 get :show, :id => 'ecookbook'
330 get :show, :id => 'ecookbook'
323 assert_response :success
331 assert_response :success
324 assert_template 'show'
332 assert_template 'show'
325 assert_no_tag :tag => 'a', :content => /Private child/
333 assert_no_tag :tag => 'a', :content => /Private child/
326 end
334 end
327
335
328 def test_private_subprojects_visible
336 def test_private_subprojects_visible
329 @request.session[:user_id] = 2 # manager who is a member of the private subproject
337 @request.session[:user_id] = 2 # manager who is a member of the private subproject
330 get :show, :id => 'ecookbook'
338 get :show, :id => 'ecookbook'
331 assert_response :success
339 assert_response :success
332 assert_template 'show'
340 assert_template 'show'
333 assert_tag :tag => 'a', :content => /Private child/
341 assert_tag :tag => 'a', :content => /Private child/
334 end
342 end
335
343
336 def test_settings
344 def test_settings
337 @request.session[:user_id] = 2 # manager
345 @request.session[:user_id] = 2 # manager
338 get :settings, :id => 1
346 get :settings, :id => 1
339 assert_response :success
347 assert_response :success
340 assert_template 'settings'
348 assert_template 'settings'
341 end
349 end
342
350
343 def test_update
351 def test_update
344 @request.session[:user_id] = 2 # manager
352 @request.session[:user_id] = 2 # manager
345 post :update, :id => 1, :project => {:name => 'Test changed name',
353 post :update, :id => 1, :project => {:name => 'Test changed name',
346 :issue_custom_field_ids => ['']}
354 :issue_custom_field_ids => ['']}
347 assert_redirected_to '/projects/ecookbook/settings'
355 assert_redirected_to '/projects/ecookbook/settings'
348 project = Project.find(1)
356 project = Project.find(1)
349 assert_equal 'Test changed name', project.name
357 assert_equal 'Test changed name', project.name
350 end
358 end
351
359
352 def test_get_destroy
360 def test_get_destroy
353 @request.session[:user_id] = 1 # admin
361 @request.session[:user_id] = 1 # admin
354 get :destroy, :id => 1
362 get :destroy, :id => 1
355 assert_response :success
363 assert_response :success
356 assert_template 'destroy'
364 assert_template 'destroy'
357 assert_not_nil Project.find_by_id(1)
365 assert_not_nil Project.find_by_id(1)
358 end
366 end
359
367
360 def test_post_destroy
368 def test_post_destroy
361 @request.session[:user_id] = 1 # admin
369 @request.session[:user_id] = 1 # admin
362 post :destroy, :id => 1, :confirm => 1
370 post :destroy, :id => 1, :confirm => 1
363 assert_redirected_to '/admin/projects'
371 assert_redirected_to '/admin/projects'
364 assert_nil Project.find_by_id(1)
372 assert_nil Project.find_by_id(1)
365 end
373 end
366
374
367 def test_archive
375 def test_archive
368 @request.session[:user_id] = 1 # admin
376 @request.session[:user_id] = 1 # admin
369 post :archive, :id => 1
377 post :archive, :id => 1
370 assert_redirected_to '/admin/projects'
378 assert_redirected_to '/admin/projects'
371 assert !Project.find(1).active?
379 assert !Project.find(1).active?
372 end
380 end
373
381
374 def test_unarchive
382 def test_unarchive
375 @request.session[:user_id] = 1 # admin
383 @request.session[:user_id] = 1 # admin
376 Project.find(1).archive
384 Project.find(1).archive
377 post :unarchive, :id => 1
385 post :unarchive, :id => 1
378 assert_redirected_to '/admin/projects'
386 assert_redirected_to '/admin/projects'
379 assert Project.find(1).active?
387 assert Project.find(1).active?
380 end
388 end
381
389
382 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
390 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
383 CustomField.delete_all
391 CustomField.delete_all
384 parent = nil
392 parent = nil
385 6.times do |i|
393 6.times do |i|
386 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
394 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
387 p.set_parent!(parent)
395 p.set_parent!(parent)
388 get :show, :id => p
396 get :show, :id => p
389 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
397 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
390 :children => { :count => [i, 3].min,
398 :children => { :count => [i, 3].min,
391 :only => { :tag => 'a' } }
399 :only => { :tag => 'a' } }
392
400
393 parent = p
401 parent = p
394 end
402 end
395 end
403 end
396
404
397 def test_copy_with_project
405 def test_copy_with_project
398 @request.session[:user_id] = 1 # admin
406 @request.session[:user_id] = 1 # admin
399 get :copy, :id => 1
407 get :copy, :id => 1
400 assert_response :success
408 assert_response :success
401 assert_template 'copy'
409 assert_template 'copy'
402 assert assigns(:project)
410 assert assigns(:project)
403 assert_equal Project.find(1).description, assigns(:project).description
411 assert_equal Project.find(1).description, assigns(:project).description
404 assert_nil assigns(:project).id
412 assert_nil assigns(:project).id
405 end
413 end
406
414
407 def test_copy_without_project
415 def test_copy_without_project
408 @request.session[:user_id] = 1 # admin
416 @request.session[:user_id] = 1 # admin
409 get :copy
417 get :copy
410 assert_response :redirect
418 assert_response :redirect
411 assert_redirected_to :controller => 'admin', :action => 'projects'
419 assert_redirected_to :controller => 'admin', :action => 'projects'
412 end
420 end
413
421
414 context "POST :copy" do
422 context "POST :copy" do
415 should "TODO: test the rest of the method"
423 should "TODO: test the rest of the method"
416
424
417 should "redirect to the project settings when successful" do
425 should "redirect to the project settings when successful" do
418 @request.session[:user_id] = 1 # admin
426 @request.session[:user_id] = 1 # admin
419 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
427 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
420 assert_response :redirect
428 assert_response :redirect
421 assert_redirected_to :controller => 'projects', :action => 'settings'
429 assert_redirected_to :controller => 'projects', :action => 'settings'
422 end
430 end
423 end
431 end
424
432
425 def test_jump_should_redirect_to_active_tab
433 def test_jump_should_redirect_to_active_tab
426 get :show, :id => 1, :jump => 'issues'
434 get :show, :id => 1, :jump => 'issues'
427 assert_redirected_to '/projects/ecookbook/issues'
435 assert_redirected_to '/projects/ecookbook/issues'
428 end
436 end
429
437
430 def test_jump_should_not_redirect_to_inactive_tab
438 def test_jump_should_not_redirect_to_inactive_tab
431 get :show, :id => 3, :jump => 'documents'
439 get :show, :id => 3, :jump => 'documents'
432 assert_response :success
440 assert_response :success
433 assert_template 'show'
441 assert_template 'show'
434 end
442 end
435
443
436 def test_jump_should_not_redirect_to_unknown_tab
444 def test_jump_should_not_redirect_to_unknown_tab
437 get :show, :id => 3, :jump => 'foobar'
445 get :show, :id => 3, :jump => 'foobar'
438 assert_response :success
446 assert_response :success
439 assert_template 'show'
447 assert_template 'show'
440 end
448 end
441
449
442 # A hook that is manually registered later
450 # A hook that is manually registered later
443 class ProjectBasedTemplate < Redmine::Hook::ViewListener
451 class ProjectBasedTemplate < Redmine::Hook::ViewListener
444 def view_layouts_base_html_head(context)
452 def view_layouts_base_html_head(context)
445 # Adds a project stylesheet
453 # Adds a project stylesheet
446 stylesheet_link_tag(context[:project].identifier) if context[:project]
454 stylesheet_link_tag(context[:project].identifier) if context[:project]
447 end
455 end
448 end
456 end
449 # Don't use this hook now
457 # Don't use this hook now
450 Redmine::Hook.clear_listeners
458 Redmine::Hook.clear_listeners
451
459
452 def test_hook_response
460 def test_hook_response
453 Redmine::Hook.add_listener(ProjectBasedTemplate)
461 Redmine::Hook.add_listener(ProjectBasedTemplate)
454 get :show, :id => 1
462 get :show, :id => 1
455 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
463 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
456 :parent => {:tag => 'head'}
464 :parent => {:tag => 'head'}
457
465
458 Redmine::Hook.clear_listeners
466 Redmine::Hook.clear_listeners
459 end
467 end
460 end
468 end
General Comments 0
You need to be logged in to leave comments. Login now