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