##// END OF EJS Templates
Display all users roles on project overview (#3339)....
Jean-Philippe Lang -
r2635:da2854cf750b
parent child
Show More
@@ -1,319 +1,319
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24 menu_item :issues, :only => [:changelog]
25 25
26 26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 27 before_filter :find_optional_project, :only => :activity
28 28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 29 before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity
31 31
32 32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 33 if controller.request.post?
34 34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 35 end
36 36 end
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 IssuesHelper
44 44 helper :queries
45 45 include QueriesHelper
46 46 helper :repositories
47 47 include RepositoriesHelper
48 48 include ProjectsHelper
49 49
50 50 # Lists visible projects
51 51 def index
52 52 respond_to do |format|
53 53 format.html {
54 54 @projects = Project.visible.find(:all, :order => 'lft')
55 55 }
56 56 format.atom {
57 57 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 58 :limit => Setting.feeds_limit.to_i)
59 59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 60 }
61 61 end
62 62 end
63 63
64 64 # Add a new project
65 65 def add
66 66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 67 @trackers = Tracker.all
68 68 @project = Project.new(params[:project])
69 69 if request.get?
70 70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 71 @project.trackers = Tracker.all
72 72 @project.is_public = Setting.default_projects_public?
73 73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 74 else
75 75 @project.enabled_module_names = params[:enabled_modules]
76 76 if @project.save
77 77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 78 flash[:notice] = l(:notice_successful_create)
79 79 redirect_to :controller => 'admin', :action => 'projects'
80 80 end
81 81 end
82 82 end
83 83
84 84 def copy
85 85 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
86 86 @trackers = Tracker.all
87 87 @root_projects = Project.find(:all,
88 88 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
89 89 :order => 'name')
90 90 if request.get?
91 91 @project = Project.copy_from(params[:id])
92 92 if @project
93 93 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
94 94 else
95 95 redirect_to :controller => 'admin', :action => 'projects'
96 96 end
97 97 else
98 98 @project = Project.new(params[:project])
99 99 @project.enabled_module_names = params[:enabled_modules]
100 100 if @project.copy(params[:id])
101 101 flash[:notice] = l(:notice_successful_create)
102 102 redirect_to :controller => 'admin', :action => 'projects'
103 103 end
104 104 end
105 105 end
106 106
107 107
108 108 # Show @project
109 109 def show
110 110 if params[:jump]
111 111 # try to redirect to the requested menu item
112 112 redirect_to_project_menu_item(@project, params[:jump]) && return
113 113 end
114 114
115 @members_by_role = @project.members.find(:all, :include => [:user, :roles], :order => 'position').group_by {|m| m.roles.first}
115 @users_by_role = @project.users_by_role
116 116 @subprojects = @project.children.visible
117 117 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
118 118 @trackers = @project.rolled_up_trackers
119 119
120 120 cond = @project.project_condition(Setting.display_subprojects_issues?)
121 121
122 122 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
123 123 :include => [:project, :status, :tracker],
124 124 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
125 125 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
126 126 :include => [:project, :status, :tracker],
127 127 :conditions => cond)
128 128
129 129 TimeEntry.visible_by(User.current) do
130 130 @total_hours = TimeEntry.sum(:hours,
131 131 :include => :project,
132 132 :conditions => cond).to_f
133 133 end
134 134 @key = User.current.rss_key
135 135 end
136 136
137 137 def settings
138 138 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
139 139 @issue_category ||= IssueCategory.new
140 140 @member ||= @project.members.new
141 141 @trackers = Tracker.all
142 142 @repository ||= @project.repository
143 143 @wiki ||= @project.wiki
144 144 end
145 145
146 146 # Edit @project
147 147 def edit
148 148 if request.post?
149 149 @project.attributes = params[:project]
150 150 if @project.save
151 151 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
152 152 flash[:notice] = l(:notice_successful_update)
153 153 redirect_to :action => 'settings', :id => @project
154 154 else
155 155 settings
156 156 render :action => 'settings'
157 157 end
158 158 end
159 159 end
160 160
161 161 def modules
162 162 @project.enabled_module_names = params[:enabled_modules]
163 163 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
164 164 end
165 165
166 166 def archive
167 167 @project.archive if request.post? && @project.active?
168 168 redirect_to :controller => 'admin', :action => 'projects'
169 169 end
170 170
171 171 def unarchive
172 172 @project.unarchive if request.post? && !@project.active?
173 173 redirect_to :controller => 'admin', :action => 'projects'
174 174 end
175 175
176 176 # Delete @project
177 177 def destroy
178 178 @project_to_destroy = @project
179 179 if request.post? and params[:confirm]
180 180 @project_to_destroy.destroy
181 181 redirect_to :controller => 'admin', :action => 'projects'
182 182 end
183 183 # hide project in layout
184 184 @project = nil
185 185 end
186 186
187 187 # Add a new issue category to @project
188 188 def add_issue_category
189 189 @category = @project.issue_categories.build(params[:category])
190 190 if request.post? and @category.save
191 191 respond_to do |format|
192 192 format.html do
193 193 flash[:notice] = l(:notice_successful_create)
194 194 redirect_to :action => 'settings', :tab => 'categories', :id => @project
195 195 end
196 196 format.js do
197 197 # IE doesn't support the replace_html rjs method for select box options
198 198 render(:update) {|page| page.replace "issue_category_id",
199 199 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
200 200 }
201 201 end
202 202 end
203 203 end
204 204 end
205 205
206 206 # Add a new version to @project
207 207 def add_version
208 208 @version = @project.versions.build(params[:version])
209 209 if request.post? and @version.save
210 210 flash[:notice] = l(:notice_successful_create)
211 211 redirect_to :action => 'settings', :tab => 'versions', :id => @project
212 212 end
213 213 end
214 214
215 215 def add_file
216 216 if request.post?
217 217 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
218 218 attachments = attach_files(container, params[:attachments])
219 219 if !attachments.empty? && Setting.notified_events.include?('file_added')
220 220 Mailer.deliver_attachments_added(attachments)
221 221 end
222 222 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
223 223 return
224 224 end
225 225 @versions = @project.versions.sort
226 226 end
227 227
228 228 def list_files
229 229 sort_init 'filename', 'asc'
230 230 sort_update 'filename' => "#{Attachment.table_name}.filename",
231 231 'created_on' => "#{Attachment.table_name}.created_on",
232 232 'size' => "#{Attachment.table_name}.filesize",
233 233 'downloads' => "#{Attachment.table_name}.downloads"
234 234
235 235 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
236 236 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
237 237 render :layout => !request.xhr?
238 238 end
239 239
240 240 # Show changelog for @project
241 241 def changelog
242 242 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
243 243 retrieve_selected_tracker_ids(@trackers)
244 244 @versions = @project.versions.sort
245 245 end
246 246
247 247 def roadmap
248 248 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
249 249 retrieve_selected_tracker_ids(@trackers)
250 250 @versions = @project.versions.sort
251 251 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
252 252 end
253 253
254 254 def activity
255 255 @days = Setting.activity_days_default.to_i
256 256
257 257 if params[:from]
258 258 begin; @date_to = params[:from].to_date + 1; rescue; end
259 259 end
260 260
261 261 @date_to ||= Date.today + 1
262 262 @date_from = @date_to - @days
263 263 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
264 264 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
265 265
266 266 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
267 267 :with_subprojects => @with_subprojects,
268 268 :author => @author)
269 269 @activity.scope_select {|t| !params["show_#{t}"].nil?}
270 270 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
271 271
272 272 events = @activity.events(@date_from, @date_to)
273 273
274 274 respond_to do |format|
275 275 format.html {
276 276 @events_by_day = events.group_by(&:event_date)
277 277 render :layout => false if request.xhr?
278 278 }
279 279 format.atom {
280 280 title = l(:label_activity)
281 281 if @author
282 282 title = @author.name
283 283 elsif @activity.scope.size == 1
284 284 title = l("label_#{@activity.scope.first.singularize}_plural")
285 285 end
286 286 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
287 287 }
288 288 end
289 289
290 290 rescue ActiveRecord::RecordNotFound
291 291 render_404
292 292 end
293 293
294 294 private
295 295 # Find project of id params[:id]
296 296 # if not found, redirect to project list
297 297 # Used as a before_filter
298 298 def find_project
299 299 @project = Project.find(params[:id])
300 300 rescue ActiveRecord::RecordNotFound
301 301 render_404
302 302 end
303 303
304 304 def find_optional_project
305 305 return true unless params[:id]
306 306 @project = Project.find(params[:id])
307 307 authorize
308 308 rescue ActiveRecord::RecordNotFound
309 309 render_404
310 310 end
311 311
312 312 def retrieve_selected_tracker_ids(selectable_trackers)
313 313 if ids = params[:tracker_ids]
314 314 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
315 315 else
316 316 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
317 317 end
318 318 end
319 319 end
@@ -1,400 +1,411
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 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 24 has_many :users, :through => :members
25 25 has_many :enabled_modules, :dependent => :delete_all
26 26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 28 has_many :issue_changes, :through => :issues, :source => :journals
29 29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 30 has_many :time_entries, :dependent => :delete_all
31 31 has_many :queries, :dependent => :delete_all
32 32 has_many :documents, :dependent => :destroy
33 33 has_many :news, :dependent => :delete_all, :include => :author
34 34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 36 has_one :repository, :dependent => :destroy
37 37 has_many :changesets, :through => :repository
38 38 has_one :wiki, :dependent => :destroy
39 39 # Custom field for the project issues
40 40 has_and_belongs_to_many :issue_custom_fields,
41 41 :class_name => 'IssueCustomField',
42 42 :order => "#{CustomField.table_name}.position",
43 43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 44 :association_foreign_key => 'custom_field_id'
45 45
46 46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 47 acts_as_attachable :view_permission => :view_files,
48 48 :delete_permission => :manage_files
49 49
50 50 acts_as_customizable
51 51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 54 :author => nil
55 55
56 56 attr_protected :status, :enabled_module_names
57 57
58 58 validates_presence_of :name, :identifier
59 59 validates_uniqueness_of :name, :identifier
60 60 validates_associated :repository, :wiki
61 61 validates_length_of :name, :maximum => 30
62 62 validates_length_of :homepage, :maximum => 255
63 63 validates_length_of :identifier, :in => 1..20
64 64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65 65
66 66 before_destroy :delete_all_members
67 67
68 68 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] } }
69 69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 70 named_scope :public, { :conditions => { :is_public => true } }
71 71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
72 72
73 73 def identifier=(identifier)
74 74 super unless identifier_frozen?
75 75 end
76 76
77 77 def identifier_frozen?
78 78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
79 79 end
80 80
81 81 def issues_with_subprojects(include_subprojects=false)
82 82 conditions = nil
83 83 if include_subprojects
84 84 ids = [id] + descendants.collect(&:id)
85 85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
86 86 end
87 87 conditions ||= ["#{Project.table_name}.id = ?", id]
88 88 # Quick and dirty fix for Rails 2 compatibility
89 89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
90 90 Version.send(:with_scope, :find => { :conditions => conditions }) do
91 91 yield
92 92 end
93 93 end
94 94 end
95 95
96 96 # returns latest created projects
97 97 # non public projects will be returned only if user is a member of those
98 98 def self.latest(user=nil, count=5)
99 99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
100 100 end
101 101
102 102 # Returns a SQL :conditions string used to find all active projects for the specified user.
103 103 #
104 104 # Examples:
105 105 # Projects.visible_by(admin) => "projects.status = 1"
106 106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
107 107 def self.visible_by(user=nil)
108 108 user ||= User.current
109 109 if user && user.admin?
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 111 elsif user && user.memberships.any?
112 112 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(',')}))"
113 113 else
114 114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
115 115 end
116 116 end
117 117
118 118 def self.allowed_to_condition(user, permission, options={})
119 119 statements = []
120 120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
121 121 if perm = Redmine::AccessControl.permission(permission)
122 122 unless perm.project_module.nil?
123 123 # If the permission belongs to a project module, make sure the module is enabled
124 124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
125 125 end
126 126 end
127 127 if options[:project]
128 128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
129 129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
130 130 base_statement = "(#{project_statement}) AND (#{base_statement})"
131 131 end
132 132 if user.admin?
133 133 # no restriction
134 134 else
135 135 statements << "1=0"
136 136 if user.logged?
137 137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
138 138 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
139 139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
140 140 elsif Role.anonymous.allowed_to?(permission)
141 141 # anonymous user allowed on public project
142 142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
143 143 else
144 144 # anonymous user is not authorized
145 145 end
146 146 end
147 147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
148 148 end
149 149
150 150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
151 151 #
152 152 # Examples:
153 153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
154 154 # project.project_condition(false) => "projects.id = 1"
155 155 def project_condition(with_subprojects)
156 156 cond = "#{Project.table_name}.id = #{id}"
157 157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
158 158 cond
159 159 end
160 160
161 161 def self.find(*args)
162 162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
163 163 project = find_by_identifier(*args)
164 164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
165 165 project
166 166 else
167 167 super
168 168 end
169 169 end
170 170
171 171 def to_param
172 172 # id is used for projects with a numeric identifier (compatibility)
173 173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
174 174 end
175 175
176 176 def active?
177 177 self.status == STATUS_ACTIVE
178 178 end
179 179
180 180 # Archives the project and its descendants recursively
181 181 def archive
182 182 # Archive subprojects if any
183 183 children.each do |subproject|
184 184 subproject.archive
185 185 end
186 186 update_attribute :status, STATUS_ARCHIVED
187 187 end
188 188
189 189 # Unarchives the project
190 190 # All its ancestors must be active
191 191 def unarchive
192 192 return false if ancestors.detect {|a| !a.active?}
193 193 update_attribute :status, STATUS_ACTIVE
194 194 end
195 195
196 196 # Returns an array of projects the project can be moved to
197 197 def possible_parents
198 198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
199 199 end
200 200
201 201 # Sets the parent of the project
202 202 # Argument can be either a Project, a String, a Fixnum or nil
203 203 def set_parent!(p)
204 204 unless p.nil? || p.is_a?(Project)
205 205 if p.to_s.blank?
206 206 p = nil
207 207 else
208 208 p = Project.find_by_id(p)
209 209 return false unless p
210 210 end
211 211 end
212 212 if p == parent && !p.nil?
213 213 # Nothing to do
214 214 true
215 215 elsif p.nil? || (p.active? && move_possible?(p))
216 216 # Insert the project so that target's children or root projects stay alphabetically sorted
217 217 sibs = (p.nil? ? self.class.roots : p.children)
218 218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
219 219 if to_be_inserted_before
220 220 move_to_left_of(to_be_inserted_before)
221 221 elsif p.nil?
222 222 if sibs.empty?
223 223 # move_to_root adds the project in first (ie. left) position
224 224 move_to_root
225 225 else
226 226 move_to_right_of(sibs.last) unless self == sibs.last
227 227 end
228 228 else
229 229 # move_to_child_of adds the project in last (ie.right) position
230 230 move_to_child_of(p)
231 231 end
232 232 true
233 233 else
234 234 # Can not move to the given target
235 235 false
236 236 end
237 237 end
238 238
239 239 # Returns an array of the trackers used by the project and its active sub projects
240 240 def rolled_up_trackers
241 241 @rolled_up_trackers ||=
242 242 Tracker.find(:all, :include => :projects,
243 243 :select => "DISTINCT #{Tracker.table_name}.*",
244 244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
245 245 :order => "#{Tracker.table_name}.position")
246 246 end
247 247
248 # Returns a hash of project users grouped by role
249 def users_by_role
250 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
251 m.roles.each do |r|
252 h[r] ||= []
253 h[r] << m.user
254 end
255 h
256 end
257 end
258
248 259 # Deletes all project's members
249 260 def delete_all_members
250 261 me, mr = Member.table_name, MemberRole.table_name
251 262 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
252 263 Member.delete_all(['project_id = ?', id])
253 264 end
254 265
255 266 # Users issues can be assigned to
256 267 def assignable_users
257 268 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
258 269 end
259 270
260 271 # Returns the mail adresses of users that should be always notified on project events
261 272 def recipients
262 273 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
263 274 end
264 275
265 276 # Returns an array of all custom fields enabled for project issues
266 277 # (explictly associated custom fields and custom fields enabled for all projects)
267 278 def all_issue_custom_fields
268 279 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
269 280 end
270 281
271 282 def project
272 283 self
273 284 end
274 285
275 286 def <=>(project)
276 287 name.downcase <=> project.name.downcase
277 288 end
278 289
279 290 def to_s
280 291 name
281 292 end
282 293
283 294 # Returns a short description of the projects (first lines)
284 295 def short_description(length = 255)
285 296 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
286 297 end
287 298
288 299 # Return true if this project is allowed to do the specified action.
289 300 # action can be:
290 301 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
291 302 # * a permission Symbol (eg. :edit_project)
292 303 def allows_to?(action)
293 304 if action.is_a? Hash
294 305 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
295 306 else
296 307 allowed_permissions.include? action
297 308 end
298 309 end
299 310
300 311 def module_enabled?(module_name)
301 312 module_name = module_name.to_s
302 313 enabled_modules.detect {|m| m.name == module_name}
303 314 end
304 315
305 316 def enabled_module_names=(module_names)
306 317 if module_names && module_names.is_a?(Array)
307 318 module_names = module_names.collect(&:to_s)
308 319 # remove disabled modules
309 320 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
310 321 # add new modules
311 322 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
312 323 else
313 324 enabled_modules.clear
314 325 end
315 326 end
316 327
317 328 # Returns an auto-generated project identifier based on the last identifier used
318 329 def self.next_identifier
319 330 p = Project.find(:first, :order => 'created_on DESC')
320 331 p.nil? ? nil : p.identifier.to_s.succ
321 332 end
322 333
323 334 # Copies and saves the Project instance based on the +project+.
324 335 # Will duplicate the source project's:
325 336 # * Issues
326 337 # * Members
327 338 # * Queries
328 339 def copy(project)
329 340 project = project.is_a?(Project) ? project : Project.find(project)
330 341
331 342 Project.transaction do
332 343 # Issues
333 344 project.issues.each do |issue|
334 345 new_issue = Issue.new
335 346 new_issue.copy_from(issue)
336 347 self.issues << new_issue
337 348 end
338 349
339 350 # Members
340 351 project.members.each do |member|
341 352 new_member = Member.new
342 353 new_member.attributes = member.attributes.dup.except("project_id")
343 354 new_member.role_ids = member.role_ids.dup
344 355 new_member.project = self
345 356 self.members << new_member
346 357 end
347 358
348 359 # Queries
349 360 project.queries.each do |query|
350 361 new_query = Query.new
351 362 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
352 363 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
353 364 new_query.project = self
354 365 self.queries << new_query
355 366 end
356 367
357 368 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
358 369 self.save
359 370 end
360 371 end
361 372
362 373
363 374 # Copies +project+ and returns the new instance. This will not save
364 375 # the copy
365 376 def self.copy_from(project)
366 377 begin
367 378 project = project.is_a?(Project) ? project : Project.find(project)
368 379 if project
369 380 # clear unique attributes
370 381 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
371 382 copy = Project.new(attributes)
372 383 copy.enabled_modules = project.enabled_modules
373 384 copy.trackers = project.trackers
374 385 copy.custom_values = project.custom_values.collect {|v| v.clone}
375 386 return copy
376 387 else
377 388 return nil
378 389 end
379 390 rescue ActiveRecord::RecordNotFound
380 391 return nil
381 392 end
382 393 end
383 394
384 395 protected
385 396 def validate
386 397 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
387 398 end
388 399
389 400 private
390 401 def allowed_permissions
391 402 @allowed_permissions ||= begin
392 403 module_names = enabled_modules.collect {|m| m.name}
393 404 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
394 405 end
395 406 end
396 407
397 408 def allowed_actions
398 409 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
399 410 end
400 411 end
@@ -1,81 +1,79
1 1 <h2><%=l(:label_overview)%></h2>
2 2
3 3 <div class="splitcontentleft">
4 4 <%= textilizable @project.description %>
5 5 <ul>
6 6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
7 7 <% if @subprojects.any? %>
8 8 <li><%=l(:label_subproject_plural)%>:
9 9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
10 10 <% end %>
11 11 <% @project.custom_values.each do |custom_value| %>
12 12 <% if !custom_value.value.empty? %>
13 13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
14 14 <% end %>
15 15 <% end %>
16 16 </ul>
17 17
18 18 <% if User.current.allowed_to?(:view_issues, @project) %>
19 19 <div class="box">
20 20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
21 21 <ul>
22 22 <% for tracker in @trackers %>
23 23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
24 24 :set_filter => 1,
25 25 "tracker_id" => tracker.id %>:
26 26 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
27 27 :total => @total_issues_by_tracker[tracker].to_i) %>
28 28 </li>
29 29 <% end %>
30 30 </ul>
31 31 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
32 32 </div>
33 33 <% end %>
34 34 <%= call_hook(:view_projects_show_left, :project => @project) %>
35 35 </div>
36 36
37 37 <div class="splitcontentright">
38 <% if @members_by_role.any? %>
38 <% if @users_by_role.any? %>
39 39 <div class="box">
40 40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 <p><% @members_by_role.keys.sort.each do |role| %>
42 <%= role.name %>:
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 <br />
41 <p><% @users_by_role.keys.sort.each do |role| %>
42 <%= role.name %>: <%= @users_by_role[role].sort.collect{|u| link_to_user u}.join(", ") %><br />
45 43 <% end %></p>
46 44 </div>
47 45 <% end %>
48 46
49 47 <% if @news.any? && authorize_for('news', 'index') %>
50 48 <div class="box">
51 49 <h3><%=l(:label_news_latest)%></h3>
52 50 <%= render :partial => 'news/news', :collection => @news %>
53 51 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 52 </div>
55 53 <% end %>
56 54 <%= call_hook(:view_projects_show_right, :project => @project) %>
57 55 </div>
58 56
59 57 <% content_for :sidebar do %>
60 58 <% planning_links = []
61 59 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
62 60 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
63 61 planning_links.compact!
64 62 unless planning_links.empty? %>
65 63 <h3><%= l(:label_planning) %></h3>
66 64 <p><%= planning_links.join(' | ') %></p>
67 65 <% end %>
68 66
69 67 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
70 68 <h3><%= l(:label_spent_time) %></h3>
71 69 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
72 70 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
73 71 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
74 72 <% end %>
75 73 <% end %>
76 74
77 75 <% content_for :header_tags do %>
78 76 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
79 77 <% end %>
80 78
81 79 <% html_title(l(:label_overview)) -%>
@@ -1,320 +1,328
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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
20 20 class ProjectTest < Test::Unit::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 end
30 30
31 31 def test_truth
32 32 assert_kind_of Project, @ecookbook
33 33 assert_equal "eCookbook", @ecookbook.name
34 34 end
35 35
36 36 def test_update
37 37 assert_equal "eCookbook", @ecookbook.name
38 38 @ecookbook.name = "eCook"
39 39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
40 40 @ecookbook.reload
41 41 assert_equal "eCook", @ecookbook.name
42 42 end
43 43
44 44 def test_validate
45 45 @ecookbook.name = ""
46 46 assert !@ecookbook.save
47 47 assert_equal 1, @ecookbook.errors.count
48 48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
49 49 end
50 50
51 51 def test_archive
52 52 user = @ecookbook.members.first.user
53 53 @ecookbook.archive
54 54 @ecookbook.reload
55 55
56 56 assert !@ecookbook.active?
57 57 assert !user.projects.include?(@ecookbook)
58 58 # Subproject are also archived
59 59 assert !@ecookbook.children.empty?
60 60 assert @ecookbook.descendants.active.empty?
61 61 end
62 62
63 63 def test_unarchive
64 64 user = @ecookbook.members.first.user
65 65 @ecookbook.archive
66 66 # A subproject of an archived project can not be unarchived
67 67 assert !@ecookbook_sub1.unarchive
68 68
69 69 # Unarchive project
70 70 assert @ecookbook.unarchive
71 71 @ecookbook.reload
72 72 assert @ecookbook.active?
73 73 assert user.projects.include?(@ecookbook)
74 74 # Subproject can now be unarchived
75 75 @ecookbook_sub1.reload
76 76 assert @ecookbook_sub1.unarchive
77 77 end
78 78
79 79 def test_destroy
80 80 # 2 active members
81 81 assert_equal 2, @ecookbook.members.size
82 82 # and 1 is locked
83 83 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
84 84 # some boards
85 85 assert @ecookbook.boards.any?
86 86
87 87 @ecookbook.destroy
88 88 # make sure that the project non longer exists
89 89 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
90 90 # make sure related data was removed
91 91 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 92 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
93 93 end
94 94
95 95 def test_move_an_orphan_project_to_a_root_project
96 96 sub = Project.find(2)
97 97 sub.set_parent! @ecookbook
98 98 assert_equal @ecookbook.id, sub.parent.id
99 99 @ecookbook.reload
100 100 assert_equal 4, @ecookbook.children.size
101 101 end
102 102
103 103 def test_move_an_orphan_project_to_a_subproject
104 104 sub = Project.find(2)
105 105 assert sub.set_parent!(@ecookbook_sub1)
106 106 end
107 107
108 108 def test_move_a_root_project_to_a_project
109 109 sub = @ecookbook
110 110 assert sub.set_parent!(Project.find(2))
111 111 end
112 112
113 113 def test_should_not_move_a_project_to_its_children
114 114 sub = @ecookbook
115 115 assert !(sub.set_parent!(Project.find(3)))
116 116 end
117 117
118 118 def test_set_parent_should_add_roots_in_alphabetical_order
119 119 ProjectCustomField.delete_all
120 120 Project.delete_all
121 121 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
122 122 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
123 123 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
124 124 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
125 125
126 126 assert_equal 4, Project.count
127 127 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
128 128 end
129 129
130 130 def test_set_parent_should_add_children_in_alphabetical_order
131 131 ProjectCustomField.delete_all
132 132 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
133 133 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
134 134 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
135 135 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
136 136 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
137 137
138 138 parent.reload
139 139 assert_equal 4, parent.children.size
140 140 assert_equal parent.children.sort_by(&:name), parent.children
141 141 end
142 142
143 143 def test_rebuild_should_sort_children_alphabetically
144 144 ProjectCustomField.delete_all
145 145 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
146 146 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
147 147 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
148 148 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
149 149 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
150 150
151 151 Project.update_all("lft = NULL, rgt = NULL")
152 152 Project.rebuild!
153 153
154 154 parent.reload
155 155 assert_equal 4, parent.children.size
156 156 assert_equal parent.children.sort_by(&:name), parent.children
157 157 end
158 158
159 159 def test_parent
160 160 p = Project.find(6).parent
161 161 assert p.is_a?(Project)
162 162 assert_equal 5, p.id
163 163 end
164 164
165 165 def test_ancestors
166 166 a = Project.find(6).ancestors
167 167 assert a.first.is_a?(Project)
168 168 assert_equal [1, 5], a.collect(&:id)
169 169 end
170 170
171 171 def test_root
172 172 r = Project.find(6).root
173 173 assert r.is_a?(Project)
174 174 assert_equal 1, r.id
175 175 end
176 176
177 177 def test_children
178 178 c = Project.find(1).children
179 179 assert c.first.is_a?(Project)
180 180 assert_equal [5, 3, 4], c.collect(&:id)
181 181 end
182 182
183 183 def test_descendants
184 184 d = Project.find(1).descendants
185 185 assert d.first.is_a?(Project)
186 186 assert_equal [5, 6, 3, 4], d.collect(&:id)
187 187 end
188 188
189 def test_users_by_role
190 users_by_role = Project.find(1).users_by_role
191 assert_kind_of Hash, users_by_role
192 role = Role.find(1)
193 assert_kind_of Array, users_by_role[role]
194 assert users_by_role[role].include?(User.find(2))
195 end
196
189 197 def test_rolled_up_trackers
190 198 parent = Project.find(1)
191 199 parent.trackers = Tracker.find([1,2])
192 200 child = parent.children.find(3)
193 201
194 202 assert_equal [1, 2], parent.tracker_ids
195 203 assert_equal [2, 3], child.trackers.collect(&:id)
196 204
197 205 assert_kind_of Tracker, parent.rolled_up_trackers.first
198 206 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
199 207
200 208 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
201 209 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
202 210 end
203 211
204 212 def test_rolled_up_trackers_should_ignore_archived_subprojects
205 213 parent = Project.find(1)
206 214 parent.trackers = Tracker.find([1,2])
207 215 child = parent.children.find(3)
208 216 child.trackers = Tracker.find([1,3])
209 217 parent.children.each(&:archive)
210 218
211 219 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
212 220 end
213 221
214 222 def test_next_identifier
215 223 ProjectCustomField.delete_all
216 224 Project.create!(:name => 'last', :identifier => 'p2008040')
217 225 assert_equal 'p2008041', Project.next_identifier
218 226 end
219 227
220 228 def test_next_identifier_first_project
221 229 Project.delete_all
222 230 assert_nil Project.next_identifier
223 231 end
224 232
225 233
226 234 def test_enabled_module_names_should_not_recreate_enabled_modules
227 235 project = Project.find(1)
228 236 # Remove one module
229 237 modules = project.enabled_modules.slice(0..-2)
230 238 assert modules.any?
231 239 assert_difference 'EnabledModule.count', -1 do
232 240 project.enabled_module_names = modules.collect(&:name)
233 241 end
234 242 project.reload
235 243 # Ids should be preserved
236 244 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
237 245 end
238 246
239 247 def test_copy_from_existing_project
240 248 source_project = Project.find(1)
241 249 copied_project = Project.copy_from(1)
242 250
243 251 assert copied_project
244 252 # Cleared attributes
245 253 assert copied_project.id.blank?
246 254 assert copied_project.name.blank?
247 255 assert copied_project.identifier.blank?
248 256
249 257 # Duplicated attributes
250 258 assert_equal source_project.description, copied_project.description
251 259 assert_equal source_project.enabled_modules, copied_project.enabled_modules
252 260 assert_equal source_project.trackers, copied_project.trackers
253 261
254 262 # Default attributes
255 263 assert_equal 1, copied_project.status
256 264 end
257 265
258 266 # Context: Project#copy
259 267 def test_copy_should_copy_issues
260 268 # Setup
261 269 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
262 270 source_project = Project.find(2)
263 271 Project.destroy_all :identifier => "copy-test"
264 272 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
265 273 project.trackers = source_project.trackers
266 274 assert project.valid?
267 275
268 276 assert project.issues.empty?
269 277 assert project.copy(source_project)
270 278
271 279 # Tests
272 280 assert_equal source_project.issues.size, project.issues.size
273 281 project.issues.each do |issue|
274 282 assert issue.valid?
275 283 assert ! issue.assigned_to.blank?
276 284 assert_equal project, issue.project
277 285 end
278 286 end
279 287
280 288 def test_copy_should_copy_members
281 289 # Setup
282 290 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
283 291 source_project = Project.find(2)
284 292 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
285 293 project.trackers = source_project.trackers
286 294 project.enabled_modules = source_project.enabled_modules
287 295 assert project.valid?
288 296
289 297 assert project.members.empty?
290 298 assert project.copy(source_project)
291 299
292 300 # Tests
293 301 assert_equal source_project.members.size, project.members.size
294 302 project.members.each do |member|
295 303 assert member
296 304 assert_equal project, member.project
297 305 end
298 306 end
299 307
300 308 def test_copy_should_copy_project_level_queries
301 309 # Setup
302 310 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
303 311 source_project = Project.find(2)
304 312 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
305 313 project.trackers = source_project.trackers
306 314 project.enabled_modules = source_project.enabled_modules
307 315 assert project.valid?
308 316
309 317 assert project.queries.empty?
310 318 assert project.copy(source_project)
311 319
312 320 # Tests
313 321 assert_equal source_project.queries.size, project.queries.size
314 322 project.queries.each do |query|
315 323 assert query
316 324 assert_equal project, query.project
317 325 end
318 326 end
319 327
320 328 end
General Comments 0
You need to be logged in to leave comments. Login now