##// END OF EJS Templates
Moves project attributes default assignments from ProjectsController#new to the model (#6064)....
Jean-Philippe Lang -
r4346:9284a32c9ac7
parent child
Show More
@@ -1,272 +1,267
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 render :template => 'projects/index.apit'
58 58 }
59 59 format.atom {
60 60 projects = Project.visible.find(:all, :order => 'created_on DESC',
61 61 :limit => Setting.feeds_limit.to_i)
62 62 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
63 63 }
64 64 end
65 65 end
66 66
67 67 def new
68 68 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
69 69 @trackers = Tracker.all
70 70 @project = Project.new(params[:project])
71
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 @project.trackers = Tracker.all
74 @project.is_public = Setting.default_projects_public?
75 @project.enabled_module_names = Setting.default_projects_modules
76 71 end
77 72
78 73 def create
79 74 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
80 75 @trackers = Tracker.all
81 76 @project = Project.new(params[:project])
82 77
83 @project.enabled_module_names = params[:enabled_modules]
78 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
84 79 if validate_parent_id && @project.save
85 80 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
86 81 # Add current user as a project member if he is not admin
87 82 unless User.current.admin?
88 83 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
89 84 m = Member.new(:user => User.current, :roles => [r])
90 85 @project.members << m
91 86 end
92 87 respond_to do |format|
93 88 format.html {
94 89 flash[:notice] = l(:notice_successful_create)
95 90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
96 91 }
97 92 format.api { render :template => 'projects/show.apit', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
98 93 end
99 94 else
100 95 respond_to do |format|
101 96 format.html { render :action => 'new' }
102 97 format.api { render_validation_errors(@project) }
103 98 end
104 99 end
105 100
106 101 end
107 102
108 103 def copy
109 104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
110 105 @trackers = Tracker.all
111 106 @root_projects = Project.find(:all,
112 107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
113 108 :order => 'name')
114 109 @source_project = Project.find(params[:id])
115 110 if request.get?
116 111 @project = Project.copy_from(@source_project)
117 112 if @project
118 113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
119 114 else
120 115 redirect_to :controller => 'admin', :action => 'projects'
121 116 end
122 117 else
123 118 Mailer.with_deliveries(params[:notifications] == '1') do
124 119 @project = Project.new(params[:project])
125 120 @project.enabled_module_names = params[:enabled_modules]
126 121 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
127 122 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
128 123 flash[:notice] = l(:notice_successful_create)
129 124 redirect_to :controller => 'projects', :action => 'settings'
130 125 elsif !@project.new_record?
131 126 # Project was created
132 127 # But some objects were not copied due to validation failures
133 128 # (eg. issues from disabled trackers)
134 129 # TODO: inform about that
135 130 redirect_to :controller => 'projects', :action => 'settings'
136 131 end
137 132 end
138 133 end
139 134 rescue ActiveRecord::RecordNotFound
140 135 redirect_to :controller => 'admin', :action => 'projects'
141 136 end
142 137
143 138 # Show @project
144 139 def show
145 140 if params[:jump]
146 141 # try to redirect to the requested menu item
147 142 redirect_to_project_menu_item(@project, params[:jump]) && return
148 143 end
149 144
150 145 @users_by_role = @project.users_by_role
151 146 @subprojects = @project.children.visible
152 147 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
153 148 @trackers = @project.rolled_up_trackers
154 149
155 150 cond = @project.project_condition(Setting.display_subprojects_issues?)
156 151
157 152 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
158 153 :include => [:project, :status, :tracker],
159 154 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
160 155 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
161 156 :include => [:project, :status, :tracker],
162 157 :conditions => cond)
163 158
164 159 TimeEntry.visible_by(User.current) do
165 160 @total_hours = TimeEntry.sum(:hours,
166 161 :include => :project,
167 162 :conditions => cond).to_f
168 163 end
169 164 @key = User.current.rss_key
170 165
171 166 respond_to do |format|
172 167 format.html
173 168 format.api { render :template => 'projects/show.apit'}
174 169 end
175 170 end
176 171
177 172 def settings
178 173 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
179 174 @issue_category ||= IssueCategory.new
180 175 @member ||= @project.members.new
181 176 @trackers = Tracker.all
182 177 @repository ||= @project.repository
183 178 @wiki ||= @project.wiki
184 179 end
185 180
186 181 def edit
187 182 end
188 183
189 184 def update
190 185 @project.attributes = params[:project]
191 186 if validate_parent_id && @project.save
192 187 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
193 188 respond_to do |format|
194 189 format.html {
195 190 flash[:notice] = l(:notice_successful_update)
196 191 redirect_to :action => 'settings', :id => @project
197 192 }
198 193 format.api { head :ok }
199 194 end
200 195 else
201 196 respond_to do |format|
202 197 format.html {
203 198 settings
204 199 render :action => 'settings'
205 200 }
206 201 format.api { render_validation_errors(@project) }
207 202 end
208 203 end
209 204 end
210 205
211 206 def modules
212 207 @project.enabled_module_names = params[:enabled_modules]
213 208 flash[:notice] = l(:notice_successful_update)
214 209 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
215 210 end
216 211
217 212 def archive
218 213 if request.post?
219 214 unless @project.archive
220 215 flash[:error] = l(:error_can_not_archive_project)
221 216 end
222 217 end
223 218 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 219 end
225 220
226 221 def unarchive
227 222 @project.unarchive if request.post? && !@project.active?
228 223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
229 224 end
230 225
231 226 # Delete @project
232 227 def destroy
233 228 @project_to_destroy = @project
234 229 if request.get?
235 230 # display confirmation view
236 231 else
237 232 if api_request? || params[:confirm]
238 233 @project_to_destroy.destroy
239 234 respond_to do |format|
240 235 format.html { redirect_to :controller => 'admin', :action => 'projects' }
241 236 format.api { head :ok }
242 237 end
243 238 end
244 239 end
245 240 # hide project in layout
246 241 @project = nil
247 242 end
248 243
249 244 private
250 245 def find_optional_project
251 246 return true unless params[:id]
252 247 @project = Project.find(params[:id])
253 248 authorize
254 249 rescue ActiveRecord::RecordNotFound
255 250 render_404
256 251 end
257 252
258 253 # Validates parent_id param according to user's permissions
259 254 # TODO: move it to Project model in a validation that depends on User.current
260 255 def validate_parent_id
261 256 return true if User.current.admin?
262 257 parent_id = params[:project] && params[:project][:parent_id]
263 258 if parent_id || @project.new_record?
264 259 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
265 260 unless @project.allowed_parents.include?(parent)
266 261 @project.errors.add :parent_id, :invalid
267 262 return false
268 263 end
269 264 end
270 265 true
271 266 end
272 267 end
@@ -1,794 +1,817
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 # Maximum length for project identifiers
24 24 IDENTIFIER_MAX_LENGTH = 100
25 25
26 26 # Specific overidden Activities
27 27 has_many :time_entry_activities
28 28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 29 has_many :memberships, :class_name => 'Member'
30 30 has_many :member_principals, :class_name => 'Member',
31 31 :include => :principal,
32 32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 33 has_many :users, :through => :members
34 34 has_many :principals, :through => :member_principals, :source => :principal
35 35
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 41 has_many :time_entries, :dependent => :delete_all
42 42 has_many :queries, :dependent => :delete_all
43 43 has_many :documents, :dependent => :destroy
44 44 has_many :news, :dependent => :delete_all, :include => :author
45 45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 47 has_one :repository, :dependent => :destroy
48 48 has_many :changesets, :through => :repository
49 49 has_one :wiki, :dependent => :destroy
50 50 # Custom field for the project issues
51 51 has_and_belongs_to_many :issue_custom_fields,
52 52 :class_name => 'IssueCustomField',
53 53 :order => "#{CustomField.table_name}.position",
54 54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 55 :association_foreign_key => 'custom_field_id'
56 56
57 57 acts_as_nested_set :order => 'name'
58 58 acts_as_attachable :view_permission => :view_files,
59 59 :delete_permission => :manage_files
60 60
61 61 acts_as_customizable
62 62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
63 63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
65 65 :author => nil
66 66
67 67 attr_protected :status, :enabled_module_names
68 68
69 69 validates_presence_of :name, :identifier
70 70 validates_uniqueness_of :identifier
71 71 validates_associated :repository, :wiki
72 72 validates_length_of :name, :maximum => 255
73 73 validates_length_of :homepage, :maximum => 255
74 74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 75 # donwcase letters, digits, dashes but not digits only
76 76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 77 # reserved words
78 78 validates_exclusion_of :identifier, :in => %w( new )
79 79
80 80 before_destroy :delete_all_members, :destroy_children
81 81
82 82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
83 83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 84 named_scope :all_public, { :conditions => { :is_public => true } }
85 85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86 86
87 def initialize(attributes = nil)
88 super
89
90 initialized = (attributes || {}).stringify_keys
91 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
92 self.identifier = Project.next_identifier
93 end
94 if !initialized.key?('is_public')
95 self.is_public = Setting.default_projects_public?
96 end
97 if !initialized.key?('enabled_module_names')
98 self.enabled_module_names = Setting.default_projects_modules
99 end
100 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
101 self.trackers = Tracker.all
102 end
103 end
104
87 105 def identifier=(identifier)
88 106 super unless identifier_frozen?
89 107 end
90 108
91 109 def identifier_frozen?
92 110 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 111 end
94 112
95 113 # returns latest created projects
96 114 # non public projects will be returned only if user is a member of those
97 115 def self.latest(user=nil, count=5)
98 116 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 117 end
100 118
101 119 # Returns a SQL :conditions string used to find all active projects for the specified user.
102 120 #
103 121 # Examples:
104 122 # Projects.visible_by(admin) => "projects.status = 1"
105 123 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
106 124 def self.visible_by(user=nil)
107 125 user ||= User.current
108 126 if user && user.admin?
109 127 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
110 128 elsif user && user.memberships.any?
111 129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
112 130 else
113 131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
114 132 end
115 133 end
116 134
117 135 def self.allowed_to_condition(user, permission, options={})
118 136 statements = []
119 137 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
120 138 if perm = Redmine::AccessControl.permission(permission)
121 139 unless perm.project_module.nil?
122 140 # If the permission belongs to a project module, make sure the module is enabled
123 141 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
124 142 end
125 143 end
126 144 if options[:project]
127 145 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
128 146 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
129 147 base_statement = "(#{project_statement}) AND (#{base_statement})"
130 148 end
131 149 if user.admin?
132 150 # no restriction
133 151 else
134 152 statements << "1=0"
135 153 if user.logged?
136 154 if Role.non_member.allowed_to?(permission) && !options[:member]
137 155 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
138 156 end
139 157 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
140 158 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
141 159 else
142 160 if Role.anonymous.allowed_to?(permission) && !options[:member]
143 161 # anonymous user allowed on public project
144 162 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
145 163 end
146 164 end
147 165 end
148 166 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
149 167 end
150 168
151 169 # Returns the Systemwide and project specific activities
152 170 def activities(include_inactive=false)
153 171 if include_inactive
154 172 return all_activities
155 173 else
156 174 return active_activities
157 175 end
158 176 end
159 177
160 178 # Will create a new Project specific Activity or update an existing one
161 179 #
162 180 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
163 181 # does not successfully save.
164 182 def update_or_create_time_entry_activity(id, activity_hash)
165 183 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
166 184 self.create_time_entry_activity_if_needed(activity_hash)
167 185 else
168 186 activity = project.time_entry_activities.find_by_id(id.to_i)
169 187 activity.update_attributes(activity_hash) if activity
170 188 end
171 189 end
172 190
173 191 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
174 192 #
175 193 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 194 # does not successfully save.
177 195 def create_time_entry_activity_if_needed(activity)
178 196 if activity['parent_id']
179 197
180 198 parent_activity = TimeEntryActivity.find(activity['parent_id'])
181 199 activity['name'] = parent_activity.name
182 200 activity['position'] = parent_activity.position
183 201
184 202 if Enumeration.overridding_change?(activity, parent_activity)
185 203 project_activity = self.time_entry_activities.create(activity)
186 204
187 205 if project_activity.new_record?
188 206 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
189 207 else
190 208 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
191 209 end
192 210 end
193 211 end
194 212 end
195 213
196 214 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
197 215 #
198 216 # Examples:
199 217 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
200 218 # project.project_condition(false) => "projects.id = 1"
201 219 def project_condition(with_subprojects)
202 220 cond = "#{Project.table_name}.id = #{id}"
203 221 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
204 222 cond
205 223 end
206 224
207 225 def self.find(*args)
208 226 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
209 227 project = find_by_identifier(*args)
210 228 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
211 229 project
212 230 else
213 231 super
214 232 end
215 233 end
216 234
217 235 def to_param
218 236 # id is used for projects with a numeric identifier (compatibility)
219 237 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
220 238 end
221 239
222 240 def active?
223 241 self.status == STATUS_ACTIVE
224 242 end
225 243
226 244 def archived?
227 245 self.status == STATUS_ARCHIVED
228 246 end
229 247
230 248 # Archives the project and its descendants
231 249 def archive
232 250 # Check that there is no issue of a non descendant project that is assigned
233 251 # to one of the project or descendant versions
234 252 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
235 253 if v_ids.any? && Issue.find(:first, :include => :project,
236 254 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
237 255 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
238 256 return false
239 257 end
240 258 Project.transaction do
241 259 archive!
242 260 end
243 261 true
244 262 end
245 263
246 264 # Unarchives the project
247 265 # All its ancestors must be active
248 266 def unarchive
249 267 return false if ancestors.detect {|a| !a.active?}
250 268 update_attribute :status, STATUS_ACTIVE
251 269 end
252 270
253 271 # Returns an array of projects the project can be moved to
254 272 # by the current user
255 273 def allowed_parents
256 274 return @allowed_parents if @allowed_parents
257 275 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
258 276 @allowed_parents = @allowed_parents - self_and_descendants
259 277 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
260 278 @allowed_parents << nil
261 279 end
262 280 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
263 281 @allowed_parents << parent
264 282 end
265 283 @allowed_parents
266 284 end
267 285
268 286 # Sets the parent of the project with authorization check
269 287 def set_allowed_parent!(p)
270 288 unless p.nil? || p.is_a?(Project)
271 289 if p.to_s.blank?
272 290 p = nil
273 291 else
274 292 p = Project.find_by_id(p)
275 293 return false unless p
276 294 end
277 295 end
278 296 if p.nil?
279 297 if !new_record? && allowed_parents.empty?
280 298 return false
281 299 end
282 300 elsif !allowed_parents.include?(p)
283 301 return false
284 302 end
285 303 set_parent!(p)
286 304 end
287 305
288 306 # Sets the parent of the project
289 307 # Argument can be either a Project, a String, a Fixnum or nil
290 308 def set_parent!(p)
291 309 unless p.nil? || p.is_a?(Project)
292 310 if p.to_s.blank?
293 311 p = nil
294 312 else
295 313 p = Project.find_by_id(p)
296 314 return false unless p
297 315 end
298 316 end
299 317 if p == parent && !p.nil?
300 318 # Nothing to do
301 319 true
302 320 elsif p.nil? || (p.active? && move_possible?(p))
303 321 # Insert the project so that target's children or root projects stay alphabetically sorted
304 322 sibs = (p.nil? ? self.class.roots : p.children)
305 323 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
306 324 if to_be_inserted_before
307 325 move_to_left_of(to_be_inserted_before)
308 326 elsif p.nil?
309 327 if sibs.empty?
310 328 # move_to_root adds the project in first (ie. left) position
311 329 move_to_root
312 330 else
313 331 move_to_right_of(sibs.last) unless self == sibs.last
314 332 end
315 333 else
316 334 # move_to_child_of adds the project in last (ie.right) position
317 335 move_to_child_of(p)
318 336 end
319 337 Issue.update_versions_from_hierarchy_change(self)
320 338 true
321 339 else
322 340 # Can not move to the given target
323 341 false
324 342 end
325 343 end
326 344
327 345 # Returns an array of the trackers used by the project and its active sub projects
328 346 def rolled_up_trackers
329 347 @rolled_up_trackers ||=
330 348 Tracker.find(:all, :include => :projects,
331 349 :select => "DISTINCT #{Tracker.table_name}.*",
332 350 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
333 351 :order => "#{Tracker.table_name}.position")
334 352 end
335 353
336 354 # Closes open and locked project versions that are completed
337 355 def close_completed_versions
338 356 Version.transaction do
339 357 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
340 358 if version.completed?
341 359 version.update_attribute(:status, 'closed')
342 360 end
343 361 end
344 362 end
345 363 end
346 364
347 365 # Returns a scope of the Versions on subprojects
348 366 def rolled_up_versions
349 367 @rolled_up_versions ||=
350 368 Version.scoped(:include => :project,
351 369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
352 370 end
353 371
354 372 # Returns a scope of the Versions used by the project
355 373 def shared_versions
356 374 @shared_versions ||=
357 375 Version.scoped(:include => :project,
358 376 :conditions => "#{Project.table_name}.id = #{id}" +
359 377 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
360 378 " #{Version.table_name}.sharing = 'system'" +
361 379 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
362 380 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
363 381 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
364 382 "))")
365 383 end
366 384
367 385 # Returns a hash of project users grouped by role
368 386 def users_by_role
369 387 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
370 388 m.roles.each do |r|
371 389 h[r] ||= []
372 390 h[r] << m.user
373 391 end
374 392 h
375 393 end
376 394 end
377 395
378 396 # Deletes all project's members
379 397 def delete_all_members
380 398 me, mr = Member.table_name, MemberRole.table_name
381 399 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
382 400 Member.delete_all(['project_id = ?', id])
383 401 end
384 402
385 403 # Users issues can be assigned to
386 404 def assignable_users
387 405 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
388 406 end
389 407
390 408 # Returns the mail adresses of users that should be always notified on project events
391 409 def recipients
392 410 notified_users.collect {|user| user.mail}
393 411 end
394 412
395 413 # Returns the users that should be notified on project events
396 414 def notified_users
397 415 # TODO: User part should be extracted to User#notify_about?
398 416 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
399 417 end
400 418
401 419 # Returns an array of all custom fields enabled for project issues
402 420 # (explictly associated custom fields and custom fields enabled for all projects)
403 421 def all_issue_custom_fields
404 422 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
405 423 end
406 424
407 425 def project
408 426 self
409 427 end
410 428
411 429 def <=>(project)
412 430 name.downcase <=> project.name.downcase
413 431 end
414 432
415 433 def to_s
416 434 name
417 435 end
418 436
419 437 # Returns a short description of the projects (first lines)
420 438 def short_description(length = 255)
421 439 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
422 440 end
423 441
424 442 def css_classes
425 443 s = 'project'
426 444 s << ' root' if root?
427 445 s << ' child' if child?
428 446 s << (leaf? ? ' leaf' : ' parent')
429 447 s
430 448 end
431 449
432 450 # The earliest start date of a project, based on it's issues and versions
433 451 def start_date
434 452 if module_enabled?(:issue_tracking)
435 453 [
436 454 issues.minimum('start_date'),
437 455 shared_versions.collect(&:effective_date),
438 456 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
439 457 ].flatten.compact.min
440 458 end
441 459 end
442 460
443 461 # The latest due date of an issue or version
444 462 def due_date
445 463 if module_enabled?(:issue_tracking)
446 464 [
447 465 issues.maximum('due_date'),
448 466 shared_versions.collect(&:effective_date),
449 467 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
450 468 ].flatten.compact.max
451 469 end
452 470 end
453 471
454 472 def overdue?
455 473 active? && !due_date.nil? && (due_date < Date.today)
456 474 end
457 475
458 476 # Returns the percent completed for this project, based on the
459 477 # progress on it's versions.
460 478 def completed_percent(options={:include_subprojects => false})
461 479 if options.delete(:include_subprojects)
462 480 total = self_and_descendants.collect(&:completed_percent).sum
463 481
464 482 total / self_and_descendants.count
465 483 else
466 484 if versions.count > 0
467 485 total = versions.collect(&:completed_pourcent).sum
468 486
469 487 total / versions.count
470 488 else
471 489 100
472 490 end
473 491 end
474 492 end
475 493
476 494 # Return true if this project is allowed to do the specified action.
477 495 # action can be:
478 496 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
479 497 # * a permission Symbol (eg. :edit_project)
480 498 def allows_to?(action)
481 499 if action.is_a? Hash
482 500 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
483 501 else
484 502 allowed_permissions.include? action
485 503 end
486 504 end
487 505
488 506 def module_enabled?(module_name)
489 507 module_name = module_name.to_s
490 508 enabled_modules.detect {|m| m.name == module_name}
491 509 end
492 510
493 511 def enabled_module_names=(module_names)
494 512 if module_names && module_names.is_a?(Array)
495 module_names = module_names.collect(&:to_s)
513 module_names = module_names.collect(&:to_s).reject(&:blank?)
496 514 # remove disabled modules
497 515 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
498 516 # add new modules
499 517 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
500 518 else
501 519 enabled_modules.clear
502 520 end
503 521 end
522
523 # Returns an array of the enabled modules names
524 def enabled_module_names
525 enabled_modules.collect(&:name)
526 end
504 527
505 528 # Returns an array of projects that are in this project's hierarchy
506 529 #
507 530 # Example: parents, children, siblings
508 531 def hierarchy
509 532 parents = project.self_and_ancestors || []
510 533 descendants = project.descendants || []
511 534 project_hierarchy = parents | descendants # Set union
512 535 end
513 536
514 537 # Returns an auto-generated project identifier based on the last identifier used
515 538 def self.next_identifier
516 539 p = Project.find(:first, :order => 'created_on DESC')
517 540 p.nil? ? nil : p.identifier.to_s.succ
518 541 end
519 542
520 543 # Copies and saves the Project instance based on the +project+.
521 544 # Duplicates the source project's:
522 545 # * Wiki
523 546 # * Versions
524 547 # * Categories
525 548 # * Issues
526 549 # * Members
527 550 # * Queries
528 551 #
529 552 # Accepts an +options+ argument to specify what to copy
530 553 #
531 554 # Examples:
532 555 # project.copy(1) # => copies everything
533 556 # project.copy(1, :only => 'members') # => copies members only
534 557 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
535 558 def copy(project, options={})
536 559 project = project.is_a?(Project) ? project : Project.find(project)
537 560
538 561 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
539 562 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
540 563
541 564 Project.transaction do
542 565 if save
543 566 reload
544 567 to_be_copied.each do |name|
545 568 send "copy_#{name}", project
546 569 end
547 570 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
548 571 save
549 572 end
550 573 end
551 574 end
552 575
553 576
554 577 # Copies +project+ and returns the new instance. This will not save
555 578 # the copy
556 579 def self.copy_from(project)
557 580 begin
558 581 project = project.is_a?(Project) ? project : Project.find(project)
559 582 if project
560 583 # clear unique attributes
561 584 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
562 585 copy = Project.new(attributes)
563 586 copy.enabled_modules = project.enabled_modules
564 587 copy.trackers = project.trackers
565 588 copy.custom_values = project.custom_values.collect {|v| v.clone}
566 589 copy.issue_custom_fields = project.issue_custom_fields
567 590 return copy
568 591 else
569 592 return nil
570 593 end
571 594 rescue ActiveRecord::RecordNotFound
572 595 return nil
573 596 end
574 597 end
575 598
576 599 # Yields the given block for each project with its level in the tree
577 600 def self.project_tree(projects, &block)
578 601 ancestors = []
579 602 projects.sort_by(&:lft).each do |project|
580 603 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
581 604 ancestors.pop
582 605 end
583 606 yield project, ancestors.size
584 607 ancestors << project
585 608 end
586 609 end
587 610
588 611 private
589 612
590 613 # Destroys children before destroying self
591 614 def destroy_children
592 615 children.each do |child|
593 616 child.destroy
594 617 end
595 618 end
596 619
597 620 # Copies wiki from +project+
598 621 def copy_wiki(project)
599 622 # Check that the source project has a wiki first
600 623 unless project.wiki.nil?
601 624 self.wiki ||= Wiki.new
602 625 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
603 626 wiki_pages_map = {}
604 627 project.wiki.pages.each do |page|
605 628 # Skip pages without content
606 629 next if page.content.nil?
607 630 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
608 631 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
609 632 new_wiki_page.content = new_wiki_content
610 633 wiki.pages << new_wiki_page
611 634 wiki_pages_map[page.id] = new_wiki_page
612 635 end
613 636 wiki.save
614 637 # Reproduce page hierarchy
615 638 project.wiki.pages.each do |page|
616 639 if page.parent_id && wiki_pages_map[page.id]
617 640 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
618 641 wiki_pages_map[page.id].save
619 642 end
620 643 end
621 644 end
622 645 end
623 646
624 647 # Copies versions from +project+
625 648 def copy_versions(project)
626 649 project.versions.each do |version|
627 650 new_version = Version.new
628 651 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
629 652 self.versions << new_version
630 653 end
631 654 end
632 655
633 656 # Copies issue categories from +project+
634 657 def copy_issue_categories(project)
635 658 project.issue_categories.each do |issue_category|
636 659 new_issue_category = IssueCategory.new
637 660 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
638 661 self.issue_categories << new_issue_category
639 662 end
640 663 end
641 664
642 665 # Copies issues from +project+
643 666 def copy_issues(project)
644 667 # Stores the source issue id as a key and the copied issues as the
645 668 # value. Used to map the two togeather for issue relations.
646 669 issues_map = {}
647 670
648 671 # Get issues sorted by root_id, lft so that parent issues
649 672 # get copied before their children
650 673 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
651 674 new_issue = Issue.new
652 675 new_issue.copy_from(issue)
653 676 new_issue.project = self
654 677 # Reassign fixed_versions by name, since names are unique per
655 678 # project and the versions for self are not yet saved
656 679 if issue.fixed_version
657 680 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
658 681 end
659 682 # Reassign the category by name, since names are unique per
660 683 # project and the categories for self are not yet saved
661 684 if issue.category
662 685 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
663 686 end
664 687 # Parent issue
665 688 if issue.parent_id
666 689 if copied_parent = issues_map[issue.parent_id]
667 690 new_issue.parent_issue_id = copied_parent.id
668 691 end
669 692 end
670 693
671 694 self.issues << new_issue
672 695 issues_map[issue.id] = new_issue
673 696 end
674 697
675 698 # Relations after in case issues related each other
676 699 project.issues.each do |issue|
677 700 new_issue = issues_map[issue.id]
678 701
679 702 # Relations
680 703 issue.relations_from.each do |source_relation|
681 704 new_issue_relation = IssueRelation.new
682 705 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
683 706 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
684 707 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
685 708 new_issue_relation.issue_to = source_relation.issue_to
686 709 end
687 710 new_issue.relations_from << new_issue_relation
688 711 end
689 712
690 713 issue.relations_to.each do |source_relation|
691 714 new_issue_relation = IssueRelation.new
692 715 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
693 716 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
694 717 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
695 718 new_issue_relation.issue_from = source_relation.issue_from
696 719 end
697 720 new_issue.relations_to << new_issue_relation
698 721 end
699 722 end
700 723 end
701 724
702 725 # Copies members from +project+
703 726 def copy_members(project)
704 727 project.memberships.each do |member|
705 728 new_member = Member.new
706 729 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
707 730 # only copy non inherited roles
708 731 # inherited roles will be added when copying the group membership
709 732 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
710 733 next if role_ids.empty?
711 734 new_member.role_ids = role_ids
712 735 new_member.project = self
713 736 self.members << new_member
714 737 end
715 738 end
716 739
717 740 # Copies queries from +project+
718 741 def copy_queries(project)
719 742 project.queries.each do |query|
720 743 new_query = Query.new
721 744 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
722 745 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
723 746 new_query.project = self
724 747 self.queries << new_query
725 748 end
726 749 end
727 750
728 751 # Copies boards from +project+
729 752 def copy_boards(project)
730 753 project.boards.each do |board|
731 754 new_board = Board.new
732 755 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
733 756 new_board.project = self
734 757 self.boards << new_board
735 758 end
736 759 end
737 760
738 761 def allowed_permissions
739 762 @allowed_permissions ||= begin
740 763 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
741 764 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
742 765 end
743 766 end
744 767
745 768 def allowed_actions
746 769 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
747 770 end
748 771
749 772 # Returns all the active Systemwide and project specific activities
750 773 def active_activities
751 774 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
752 775
753 776 if overridden_activity_ids.empty?
754 777 return TimeEntryActivity.shared.active
755 778 else
756 779 return system_activities_and_project_overrides
757 780 end
758 781 end
759 782
760 783 # Returns all the Systemwide and project specific activities
761 784 # (inactive and active)
762 785 def all_activities
763 786 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
764 787
765 788 if overridden_activity_ids.empty?
766 789 return TimeEntryActivity.shared
767 790 else
768 791 return system_activities_and_project_overrides(true)
769 792 end
770 793 end
771 794
772 795 # Returns the systemwide active activities merged with the project specific overrides
773 796 def system_activities_and_project_overrides(include_inactive=false)
774 797 if include_inactive
775 798 return TimeEntryActivity.shared.
776 799 find(:all,
777 800 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
778 801 self.time_entry_activities
779 802 else
780 803 return TimeEntryActivity.shared.active.
781 804 find(:all,
782 805 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
783 806 self.time_entry_activities.active
784 807 end
785 808 end
786 809
787 810 # Archives subprojects recursively
788 811 def archive!
789 812 children.each do |subproject|
790 813 subproject.send :archive!
791 814 end
792 815 update_attribute :status, STATUS_ARCHIVED
793 816 end
794 817 end
@@ -1,17 +1,19
1 1 <h2><%=l(:label_project_new)%></h2>
2 2
3 3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5
6 6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 8 <label class="floating">
9 9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 11 </label>
12 12 <% end %>
13 <%= hidden_field_tag 'enabled_modules[]', '' %>
14
13 15 </fieldset>
14 16
15 17 <%= submit_tag l(:button_save) %>
16 18 <%= javascript_tag "Form.Element.focus('project_name');" %>
17 19 <% end %>
@@ -1,214 +1,216
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 require "#{File.dirname(__FILE__)}/../../test_helper"
19 19
20 20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 23 :attachments, :custom_fields, :custom_values, :time_entries
24 24
25 25 def setup
26 26 Setting.rest_api_enabled = '1'
27 27 end
28 28
29 29 context "GET /projects" do
30 30 context ".xml" do
31 31 should "return projects" do
32 32 get '/projects.xml'
33 33 assert_response :success
34 34 assert_equal 'application/xml', @response.content_type
35 35
36 36 assert_tag :tag => 'projects',
37 37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 38 end
39 39 end
40 40
41 41 context ".json" do
42 42 should "return projects" do
43 43 get '/projects.json'
44 44 assert_response :success
45 45 assert_equal 'application/json', @response.content_type
46 46
47 47 json = ActiveSupport::JSON.decode(response.body)
48 48 assert_kind_of Hash, json
49 49 assert_kind_of Array, json['projects']
50 50 assert_kind_of Hash, json['projects'].first
51 51 assert json['projects'].first.has_key?('id')
52 52 end
53 53 end
54 54 end
55 55
56 56 context "GET /projects/:id" do
57 57 context ".xml" do
58 58 # TODO: A private project is needed because should_allow_api_authentication
59 59 # actually tests that authentication is *required*, not just allowed
60 60 should_allow_api_authentication(:get, "/projects/2.xml")
61 61
62 62 should "return requested project" do
63 63 get '/projects/1.xml'
64 64 assert_response :success
65 65 assert_equal 'application/xml', @response.content_type
66 66
67 67 assert_tag :tag => 'project',
68 68 :child => {:tag => 'id', :content => '1'}
69 69 assert_tag :tag => 'custom_field',
70 70 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 71 end
72 72
73 73 context "with hidden custom fields" do
74 74 setup do
75 75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
76 76 end
77 77
78 78 should "not display hidden custom fields" do
79 79 get '/projects/1.xml'
80 80 assert_response :success
81 81 assert_equal 'application/xml', @response.content_type
82 82
83 83 assert_no_tag 'custom_field',
84 84 :attributes => {:name => 'Development status'}
85 85 end
86 86 end
87 87 end
88 88
89 89 context ".json" do
90 90 should_allow_api_authentication(:get, "/projects/2.json")
91 91
92 92 should "return requested project" do
93 93 get '/projects/1.json'
94 94
95 95 json = ActiveSupport::JSON.decode(response.body)
96 96 assert_kind_of Hash, json
97 97 assert_kind_of Hash, json['project']
98 98 assert_equal 1, json['project']['id']
99 99 end
100 100 end
101 101 end
102 102
103 103 context "POST /projects" do
104 104 context "with valid parameters" do
105 105 setup do
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
106 107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
107 108 end
108 109
109 110 context ".xml" do
110 111 should_allow_api_authentication(:post,
111 112 '/projects.xml',
112 113 {:project => {:name => 'API test', :identifier => 'api-test'}},
113 114 {:success_code => :created})
114 115
115 116
116 117 should "create a project with the attributes" do
117 118 assert_difference('Project.count') do
118 119 post '/projects.xml', @parameters, :authorization => credentials('admin')
119 120 end
120 121
121 122 project = Project.first(:order => 'id DESC')
122 123 assert_equal 'API test', project.name
123 124 assert_equal 'api-test', project.identifier
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names
124 126
125 127 assert_response :created
126 128 assert_equal 'application/xml', @response.content_type
127 129 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
128 130 end
129 131 end
130 132 end
131 133
132 134 context "with invalid parameters" do
133 135 setup do
134 136 @parameters = {:project => {:name => 'API test'}}
135 137 end
136 138
137 139 context ".xml" do
138 140 should "return errors" do
139 141 assert_no_difference('Project.count') do
140 142 post '/projects.xml', @parameters, :authorization => credentials('admin')
141 143 end
142 144
143 145 assert_response :unprocessable_entity
144 146 assert_equal 'application/xml', @response.content_type
145 147 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
146 148 end
147 149 end
148 150 end
149 151 end
150 152
151 153 context "PUT /projects/:id" do
152 154 context "with valid parameters" do
153 155 setup do
154 156 @parameters = {:project => {:name => 'API update'}}
155 157 end
156 158
157 159 context ".xml" do
158 160 should_allow_api_authentication(:put,
159 161 '/projects/2.xml',
160 162 {:project => {:name => 'API update'}},
161 163 {:success_code => :ok})
162 164
163 165 should "update the project" do
164 166 assert_no_difference 'Project.count' do
165 167 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
166 168 end
167 169 assert_response :ok
168 170 assert_equal 'application/xml', @response.content_type
169 171 project = Project.find(2)
170 172 assert_equal 'API update', project.name
171 173 end
172 174 end
173 175 end
174 176
175 177 context "with invalid parameters" do
176 178 setup do
177 179 @parameters = {:project => {:name => ''}}
178 180 end
179 181
180 182 context ".xml" do
181 183 should "return errors" do
182 184 assert_no_difference('Project.count') do
183 185 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
184 186 end
185 187
186 188 assert_response :unprocessable_entity
187 189 assert_equal 'application/xml', @response.content_type
188 190 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
189 191 end
190 192 end
191 193 end
192 194 end
193 195
194 196 context "DELETE /projects/:id" do
195 197 context ".xml" do
196 198 should_allow_api_authentication(:delete,
197 199 '/projects/2.xml',
198 200 {},
199 201 {:success_code => :ok})
200 202
201 203 should "delete the project" do
202 204 assert_difference('Project.count',-1) do
203 205 delete '/projects/2.xml', {}, :authorization => credentials('admin')
204 206 end
205 207 assert_response :ok
206 208 assert_nil Project.find_by_id(2)
207 209 end
208 210 end
209 211 end
210 212
211 213 def credentials(user, password=nil)
212 214 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
213 215 end
214 216 end
@@ -1,1018 +1,1047
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 < ActiveSupport::TestCase
21 21 fixtures :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 32 should_validate_uniqueness_of :identifier
33 33
34 34 context "associations" do
35 35 should_have_many :members
36 36 should_have_many :users, :through => :members
37 37 should_have_many :member_principals
38 38 should_have_many :principals, :through => :member_principals
39 39 should_have_many :enabled_modules
40 40 should_have_many :issues
41 41 should_have_many :issue_changes, :through => :issues
42 42 should_have_many :versions
43 43 should_have_many :time_entries
44 44 should_have_many :queries
45 45 should_have_many :documents
46 46 should_have_many :news
47 47 should_have_many :issue_categories
48 48 should_have_many :boards
49 49 should_have_many :changesets, :through => :repository
50 50
51 51 should_have_one :repository
52 52 should_have_one :wiki
53 53
54 54 should_have_and_belong_to_many :trackers
55 55 should_have_and_belong_to_many :issue_custom_fields
56 56 end
57 57
58 58 def test_truth
59 59 assert_kind_of Project, @ecookbook
60 60 assert_equal "eCookbook", @ecookbook.name
61 61 end
62 62
63 def test_default_attributes
64 with_settings :default_projects_public => '1' do
65 assert_equal true, Project.new.is_public
66 assert_equal false, Project.new(:is_public => false).is_public
67 end
68
69 with_settings :default_projects_public => '0' do
70 assert_equal false, Project.new.is_public
71 assert_equal true, Project.new(:is_public => true).is_public
72 end
73
74 with_settings :sequential_project_identifiers => '1' do
75 assert !Project.new.identifier.blank?
76 assert Project.new(:identifier => '').identifier.blank?
77 end
78
79 with_settings :sequential_project_identifiers => '0' do
80 assert Project.new.identifier.blank?
81 assert !Project.new(:identifier => 'test').blank?
82 end
83
84 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
85 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
86 end
87
88 assert_equal Tracker.all, Project.new.trackers
89 assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers
90 end
91
63 92 def test_update
64 93 assert_equal "eCookbook", @ecookbook.name
65 94 @ecookbook.name = "eCook"
66 95 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
67 96 @ecookbook.reload
68 97 assert_equal "eCook", @ecookbook.name
69 98 end
70 99
71 100 def test_validate_identifier
72 101 to_test = {"abc" => true,
73 102 "ab12" => true,
74 103 "ab-12" => true,
75 104 "12" => false,
76 105 "new" => false}
77 106
78 107 to_test.each do |identifier, valid|
79 108 p = Project.new
80 109 p.identifier = identifier
81 110 p.valid?
82 111 assert_equal valid, p.errors.on('identifier').nil?
83 112 end
84 113 end
85 114
86 115 def test_members_should_be_active_users
87 116 Project.all.each do |project|
88 117 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
89 118 end
90 119 end
91 120
92 121 def test_users_should_be_active_users
93 122 Project.all.each do |project|
94 123 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
95 124 end
96 125 end
97 126
98 127 def test_archive
99 128 user = @ecookbook.members.first.user
100 129 @ecookbook.archive
101 130 @ecookbook.reload
102 131
103 132 assert !@ecookbook.active?
104 133 assert @ecookbook.archived?
105 134 assert !user.projects.include?(@ecookbook)
106 135 # Subproject are also archived
107 136 assert !@ecookbook.children.empty?
108 137 assert @ecookbook.descendants.active.empty?
109 138 end
110 139
111 140 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 141 # Assign an issue of a project to a version of a child project
113 142 Issue.find(4).update_attribute :fixed_version_id, 4
114 143
115 144 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 145 assert_equal false, @ecookbook.archive
117 146 end
118 147 @ecookbook.reload
119 148 assert @ecookbook.active?
120 149 end
121 150
122 151 def test_unarchive
123 152 user = @ecookbook.members.first.user
124 153 @ecookbook.archive
125 154 # A subproject of an archived project can not be unarchived
126 155 assert !@ecookbook_sub1.unarchive
127 156
128 157 # Unarchive project
129 158 assert @ecookbook.unarchive
130 159 @ecookbook.reload
131 160 assert @ecookbook.active?
132 161 assert !@ecookbook.archived?
133 162 assert user.projects.include?(@ecookbook)
134 163 # Subproject can now be unarchived
135 164 @ecookbook_sub1.reload
136 165 assert @ecookbook_sub1.unarchive
137 166 end
138 167
139 168 def test_destroy
140 169 # 2 active members
141 170 assert_equal 2, @ecookbook.members.size
142 171 # and 1 is locked
143 172 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
144 173 # some boards
145 174 assert @ecookbook.boards.any?
146 175
147 176 @ecookbook.destroy
148 177 # make sure that the project non longer exists
149 178 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
150 179 # make sure related data was removed
151 180 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
152 181 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
153 182 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
154 183 end
155 184
156 185 def test_move_an_orphan_project_to_a_root_project
157 186 sub = Project.find(2)
158 187 sub.set_parent! @ecookbook
159 188 assert_equal @ecookbook.id, sub.parent.id
160 189 @ecookbook.reload
161 190 assert_equal 4, @ecookbook.children.size
162 191 end
163 192
164 193 def test_move_an_orphan_project_to_a_subproject
165 194 sub = Project.find(2)
166 195 assert sub.set_parent!(@ecookbook_sub1)
167 196 end
168 197
169 198 def test_move_a_root_project_to_a_project
170 199 sub = @ecookbook
171 200 assert sub.set_parent!(Project.find(2))
172 201 end
173 202
174 203 def test_should_not_move_a_project_to_its_children
175 204 sub = @ecookbook
176 205 assert !(sub.set_parent!(Project.find(3)))
177 206 end
178 207
179 208 def test_set_parent_should_add_roots_in_alphabetical_order
180 209 ProjectCustomField.delete_all
181 210 Project.delete_all
182 211 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
183 212 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
184 213 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
185 214 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
186 215
187 216 assert_equal 4, Project.count
188 217 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
189 218 end
190 219
191 220 def test_set_parent_should_add_children_in_alphabetical_order
192 221 ProjectCustomField.delete_all
193 222 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
194 223 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
195 224 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
196 225 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
197 226 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
198 227
199 228 parent.reload
200 229 assert_equal 4, parent.children.size
201 230 assert_equal parent.children.sort_by(&:name), parent.children
202 231 end
203 232
204 233 def test_rebuild_should_sort_children_alphabetically
205 234 ProjectCustomField.delete_all
206 235 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
207 236 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
208 237 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
209 238 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
210 239 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
211 240
212 241 Project.update_all("lft = NULL, rgt = NULL")
213 242 Project.rebuild!
214 243
215 244 parent.reload
216 245 assert_equal 4, parent.children.size
217 246 assert_equal parent.children.sort_by(&:name), parent.children
218 247 end
219 248
220 249
221 250 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
222 251 # Parent issue with a hierarchy project's fixed version
223 252 parent_issue = Issue.find(1)
224 253 parent_issue.update_attribute(:fixed_version_id, 4)
225 254 parent_issue.reload
226 255 assert_equal 4, parent_issue.fixed_version_id
227 256
228 257 # Should keep fixed versions for the issues
229 258 issue_with_local_fixed_version = Issue.find(5)
230 259 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
231 260 issue_with_local_fixed_version.reload
232 261 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
233 262
234 263 # Local issue with hierarchy fixed_version
235 264 issue_with_hierarchy_fixed_version = Issue.find(13)
236 265 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
237 266 issue_with_hierarchy_fixed_version.reload
238 267 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
239 268
240 269 # Move project out of the issue's hierarchy
241 270 moved_project = Project.find(3)
242 271 moved_project.set_parent!(Project.find(2))
243 272 parent_issue.reload
244 273 issue_with_local_fixed_version.reload
245 274 issue_with_hierarchy_fixed_version.reload
246 275
247 276 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
248 277 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
249 278 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
250 279 end
251 280
252 281 def test_parent
253 282 p = Project.find(6).parent
254 283 assert p.is_a?(Project)
255 284 assert_equal 5, p.id
256 285 end
257 286
258 287 def test_ancestors
259 288 a = Project.find(6).ancestors
260 289 assert a.first.is_a?(Project)
261 290 assert_equal [1, 5], a.collect(&:id)
262 291 end
263 292
264 293 def test_root
265 294 r = Project.find(6).root
266 295 assert r.is_a?(Project)
267 296 assert_equal 1, r.id
268 297 end
269 298
270 299 def test_children
271 300 c = Project.find(1).children
272 301 assert c.first.is_a?(Project)
273 302 assert_equal [5, 3, 4], c.collect(&:id)
274 303 end
275 304
276 305 def test_descendants
277 306 d = Project.find(1).descendants
278 307 assert d.first.is_a?(Project)
279 308 assert_equal [5, 6, 3, 4], d.collect(&:id)
280 309 end
281 310
282 311 def test_allowed_parents_should_be_empty_for_non_member_user
283 312 Role.non_member.add_permission!(:add_project)
284 313 user = User.find(9)
285 314 assert user.memberships.empty?
286 315 User.current = user
287 316 assert Project.new.allowed_parents.compact.empty?
288 317 end
289 318
290 319 def test_allowed_parents_with_add_subprojects_permission
291 320 Role.find(1).remove_permission!(:add_project)
292 321 Role.find(1).add_permission!(:add_subprojects)
293 322 User.current = User.find(2)
294 323 # new project
295 324 assert !Project.new.allowed_parents.include?(nil)
296 325 assert Project.new.allowed_parents.include?(Project.find(1))
297 326 # existing root project
298 327 assert Project.find(1).allowed_parents.include?(nil)
299 328 # existing child
300 329 assert Project.find(3).allowed_parents.include?(Project.find(1))
301 330 assert !Project.find(3).allowed_parents.include?(nil)
302 331 end
303 332
304 333 def test_allowed_parents_with_add_project_permission
305 334 Role.find(1).add_permission!(:add_project)
306 335 Role.find(1).remove_permission!(:add_subprojects)
307 336 User.current = User.find(2)
308 337 # new project
309 338 assert Project.new.allowed_parents.include?(nil)
310 339 assert !Project.new.allowed_parents.include?(Project.find(1))
311 340 # existing root project
312 341 assert Project.find(1).allowed_parents.include?(nil)
313 342 # existing child
314 343 assert Project.find(3).allowed_parents.include?(Project.find(1))
315 344 assert Project.find(3).allowed_parents.include?(nil)
316 345 end
317 346
318 347 def test_allowed_parents_with_add_project_and_subprojects_permission
319 348 Role.find(1).add_permission!(:add_project)
320 349 Role.find(1).add_permission!(:add_subprojects)
321 350 User.current = User.find(2)
322 351 # new project
323 352 assert Project.new.allowed_parents.include?(nil)
324 353 assert Project.new.allowed_parents.include?(Project.find(1))
325 354 # existing root project
326 355 assert Project.find(1).allowed_parents.include?(nil)
327 356 # existing child
328 357 assert Project.find(3).allowed_parents.include?(Project.find(1))
329 358 assert Project.find(3).allowed_parents.include?(nil)
330 359 end
331 360
332 361 def test_users_by_role
333 362 users_by_role = Project.find(1).users_by_role
334 363 assert_kind_of Hash, users_by_role
335 364 role = Role.find(1)
336 365 assert_kind_of Array, users_by_role[role]
337 366 assert users_by_role[role].include?(User.find(2))
338 367 end
339 368
340 369 def test_rolled_up_trackers
341 370 parent = Project.find(1)
342 371 parent.trackers = Tracker.find([1,2])
343 372 child = parent.children.find(3)
344 373
345 374 assert_equal [1, 2], parent.tracker_ids
346 375 assert_equal [2, 3], child.trackers.collect(&:id)
347 376
348 377 assert_kind_of Tracker, parent.rolled_up_trackers.first
349 378 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
350 379
351 380 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
352 381 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
353 382 end
354 383
355 384 def test_rolled_up_trackers_should_ignore_archived_subprojects
356 385 parent = Project.find(1)
357 386 parent.trackers = Tracker.find([1,2])
358 387 child = parent.children.find(3)
359 388 child.trackers = Tracker.find([1,3])
360 389 parent.children.each(&:archive)
361 390
362 391 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
363 392 end
364 393
365 394 context "#rolled_up_versions" do
366 395 setup do
367 396 @project = Project.generate!
368 397 @parent_version_1 = Version.generate!(:project => @project)
369 398 @parent_version_2 = Version.generate!(:project => @project)
370 399 end
371 400
372 401 should "include the versions for the current project" do
373 402 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
374 403 end
375 404
376 405 should "include versions for a subproject" do
377 406 @subproject = Project.generate!
378 407 @subproject.set_parent!(@project)
379 408 @subproject_version = Version.generate!(:project => @subproject)
380 409
381 410 assert_same_elements [
382 411 @parent_version_1,
383 412 @parent_version_2,
384 413 @subproject_version
385 414 ], @project.rolled_up_versions
386 415 end
387 416
388 417 should "include versions for a sub-subproject" do
389 418 @subproject = Project.generate!
390 419 @subproject.set_parent!(@project)
391 420 @sub_subproject = Project.generate!
392 421 @sub_subproject.set_parent!(@subproject)
393 422 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
394 423
395 424 @project.reload
396 425
397 426 assert_same_elements [
398 427 @parent_version_1,
399 428 @parent_version_2,
400 429 @sub_subproject_version
401 430 ], @project.rolled_up_versions
402 431 end
403 432
404 433
405 434 should "only check active projects" do
406 435 @subproject = Project.generate!
407 436 @subproject.set_parent!(@project)
408 437 @subproject_version = Version.generate!(:project => @subproject)
409 438 assert @subproject.archive
410 439
411 440 @project.reload
412 441
413 442 assert !@subproject.active?
414 443 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
415 444 end
416 445 end
417 446
418 447 def test_shared_versions_none_sharing
419 448 p = Project.find(5)
420 449 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
421 450 assert p.shared_versions.include?(v)
422 451 assert !p.children.first.shared_versions.include?(v)
423 452 assert !p.root.shared_versions.include?(v)
424 453 assert !p.siblings.first.shared_versions.include?(v)
425 454 assert !p.root.siblings.first.shared_versions.include?(v)
426 455 end
427 456
428 457 def test_shared_versions_descendants_sharing
429 458 p = Project.find(5)
430 459 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
431 460 assert p.shared_versions.include?(v)
432 461 assert p.children.first.shared_versions.include?(v)
433 462 assert !p.root.shared_versions.include?(v)
434 463 assert !p.siblings.first.shared_versions.include?(v)
435 464 assert !p.root.siblings.first.shared_versions.include?(v)
436 465 end
437 466
438 467 def test_shared_versions_hierarchy_sharing
439 468 p = Project.find(5)
440 469 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
441 470 assert p.shared_versions.include?(v)
442 471 assert p.children.first.shared_versions.include?(v)
443 472 assert p.root.shared_versions.include?(v)
444 473 assert !p.siblings.first.shared_versions.include?(v)
445 474 assert !p.root.siblings.first.shared_versions.include?(v)
446 475 end
447 476
448 477 def test_shared_versions_tree_sharing
449 478 p = Project.find(5)
450 479 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
451 480 assert p.shared_versions.include?(v)
452 481 assert p.children.first.shared_versions.include?(v)
453 482 assert p.root.shared_versions.include?(v)
454 483 assert p.siblings.first.shared_versions.include?(v)
455 484 assert !p.root.siblings.first.shared_versions.include?(v)
456 485 end
457 486
458 487 def test_shared_versions_system_sharing
459 488 p = Project.find(5)
460 489 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
461 490 assert p.shared_versions.include?(v)
462 491 assert p.children.first.shared_versions.include?(v)
463 492 assert p.root.shared_versions.include?(v)
464 493 assert p.siblings.first.shared_versions.include?(v)
465 494 assert p.root.siblings.first.shared_versions.include?(v)
466 495 end
467 496
468 497 def test_shared_versions
469 498 parent = Project.find(1)
470 499 child = parent.children.find(3)
471 500 private_child = parent.children.find(5)
472 501
473 502 assert_equal [1,2,3], parent.version_ids.sort
474 503 assert_equal [4], child.version_ids
475 504 assert_equal [6], private_child.version_ids
476 505 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
477 506
478 507 assert_equal 6, parent.shared_versions.size
479 508 parent.shared_versions.each do |version|
480 509 assert_kind_of Version, version
481 510 end
482 511
483 512 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
484 513 end
485 514
486 515 def test_shared_versions_should_ignore_archived_subprojects
487 516 parent = Project.find(1)
488 517 child = parent.children.find(3)
489 518 child.archive
490 519 parent.reload
491 520
492 521 assert_equal [1,2,3], parent.version_ids.sort
493 522 assert_equal [4], child.version_ids
494 523 assert !parent.shared_versions.collect(&:id).include?(4)
495 524 end
496 525
497 526 def test_shared_versions_visible_to_user
498 527 user = User.find(3)
499 528 parent = Project.find(1)
500 529 child = parent.children.find(5)
501 530
502 531 assert_equal [1,2,3], parent.version_ids.sort
503 532 assert_equal [6], child.version_ids
504 533
505 534 versions = parent.shared_versions.visible(user)
506 535
507 536 assert_equal 4, versions.size
508 537 versions.each do |version|
509 538 assert_kind_of Version, version
510 539 end
511 540
512 541 assert !versions.collect(&:id).include?(6)
513 542 end
514 543
515 544
516 545 def test_next_identifier
517 546 ProjectCustomField.delete_all
518 547 Project.create!(:name => 'last', :identifier => 'p2008040')
519 548 assert_equal 'p2008041', Project.next_identifier
520 549 end
521 550
522 551 def test_next_identifier_first_project
523 552 Project.delete_all
524 553 assert_nil Project.next_identifier
525 554 end
526 555
527 556
528 557 def test_enabled_module_names_should_not_recreate_enabled_modules
529 558 project = Project.find(1)
530 559 # Remove one module
531 560 modules = project.enabled_modules.slice(0..-2)
532 561 assert modules.any?
533 562 assert_difference 'EnabledModule.count', -1 do
534 563 project.enabled_module_names = modules.collect(&:name)
535 564 end
536 565 project.reload
537 566 # Ids should be preserved
538 567 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
539 568 end
540 569
541 570 def test_copy_from_existing_project
542 571 source_project = Project.find(1)
543 572 copied_project = Project.copy_from(1)
544 573
545 574 assert copied_project
546 575 # Cleared attributes
547 576 assert copied_project.id.blank?
548 577 assert copied_project.name.blank?
549 578 assert copied_project.identifier.blank?
550 579
551 580 # Duplicated attributes
552 581 assert_equal source_project.description, copied_project.description
553 582 assert_equal source_project.enabled_modules, copied_project.enabled_modules
554 583 assert_equal source_project.trackers, copied_project.trackers
555 584
556 585 # Default attributes
557 586 assert_equal 1, copied_project.status
558 587 end
559 588
560 589 def test_activities_should_use_the_system_activities
561 590 project = Project.find(1)
562 591 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
563 592 end
564 593
565 594
566 595 def test_activities_should_use_the_project_specific_activities
567 596 project = Project.find(1)
568 597 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
569 598 assert overridden_activity.save!
570 599
571 600 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
572 601 end
573 602
574 603 def test_activities_should_not_include_the_inactive_project_specific_activities
575 604 project = Project.find(1)
576 605 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
577 606 assert overridden_activity.save!
578 607
579 608 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
580 609 end
581 610
582 611 def test_activities_should_not_include_project_specific_activities_from_other_projects
583 612 project = Project.find(1)
584 613 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
585 614 assert overridden_activity.save!
586 615
587 616 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
588 617 end
589 618
590 619 def test_activities_should_handle_nils
591 620 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
592 621 TimeEntryActivity.delete_all
593 622
594 623 # No activities
595 624 project = Project.find(1)
596 625 assert project.activities.empty?
597 626
598 627 # No system, one overridden
599 628 assert overridden_activity.save!
600 629 project.reload
601 630 assert_equal [overridden_activity], project.activities
602 631 end
603 632
604 633 def test_activities_should_override_system_activities_with_project_activities
605 634 project = Project.find(1)
606 635 parent_activity = TimeEntryActivity.find(:first)
607 636 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
608 637 assert overridden_activity.save!
609 638
610 639 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
611 640 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
612 641 end
613 642
614 643 def test_activities_should_include_inactive_activities_if_specified
615 644 project = Project.find(1)
616 645 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
617 646 assert overridden_activity.save!
618 647
619 648 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
620 649 end
621 650
622 651 test 'activities should not include active System activities if the project has an override that is inactive' do
623 652 project = Project.find(1)
624 653 system_activity = TimeEntryActivity.find_by_name('Design')
625 654 assert system_activity.active?
626 655 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
627 656 assert overridden_activity.save!
628 657
629 658 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
630 659 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
631 660 end
632 661
633 662 def test_close_completed_versions
634 663 Version.update_all("status = 'open'")
635 664 project = Project.find(1)
636 665 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
637 666 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
638 667 project.close_completed_versions
639 668 project.reload
640 669 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
641 670 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
642 671 end
643 672
644 673 context "Project#copy" do
645 674 setup do
646 675 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
647 676 Project.destroy_all :identifier => "copy-test"
648 677 @source_project = Project.find(2)
649 678 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
650 679 @project.trackers = @source_project.trackers
651 680 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
652 681 end
653 682
654 683 should "copy issues" do
655 684 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
656 685 :subject => "copy issue status",
657 686 :tracker_id => 1,
658 687 :assigned_to_id => 2,
659 688 :project_id => @source_project.id)
660 689 assert @project.valid?
661 690 assert @project.issues.empty?
662 691 assert @project.copy(@source_project)
663 692
664 693 assert_equal @source_project.issues.size, @project.issues.size
665 694 @project.issues.each do |issue|
666 695 assert issue.valid?
667 696 assert ! issue.assigned_to.blank?
668 697 assert_equal @project, issue.project
669 698 end
670 699
671 700 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
672 701 assert copied_issue
673 702 assert copied_issue.status
674 703 assert_equal "Closed", copied_issue.status.name
675 704 end
676 705
677 706 should "change the new issues to use the copied version" do
678 707 User.current = User.find(1)
679 708 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
680 709 @source_project.versions << assigned_version
681 710 assert_equal 3, @source_project.versions.size
682 711 Issue.generate_for_project!(@source_project,
683 712 :fixed_version_id => assigned_version.id,
684 713 :subject => "change the new issues to use the copied version",
685 714 :tracker_id => 1,
686 715 :project_id => @source_project.id)
687 716
688 717 assert @project.copy(@source_project)
689 718 @project.reload
690 719 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
691 720
692 721 assert copied_issue
693 722 assert copied_issue.fixed_version
694 723 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
695 724 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
696 725 end
697 726
698 727 should "copy issue relations" do
699 728 Setting.cross_project_issue_relations = '1'
700 729
701 730 second_issue = Issue.generate!(:status_id => 5,
702 731 :subject => "copy issue relation",
703 732 :tracker_id => 1,
704 733 :assigned_to_id => 2,
705 734 :project_id => @source_project.id)
706 735 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
707 736 :issue_to => second_issue,
708 737 :relation_type => "relates")
709 738 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
710 739 :issue_to => second_issue,
711 740 :relation_type => "duplicates")
712 741
713 742 assert @project.copy(@source_project)
714 743 assert_equal @source_project.issues.count, @project.issues.count
715 744 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
716 745 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
717 746
718 747 # First issue with a relation on project
719 748 assert_equal 1, copied_issue.relations.size, "Relation not copied"
720 749 copied_relation = copied_issue.relations.first
721 750 assert_equal "relates", copied_relation.relation_type
722 751 assert_equal copied_second_issue.id, copied_relation.issue_to_id
723 752 assert_not_equal source_relation.id, copied_relation.id
724 753
725 754 # Second issue with a cross project relation
726 755 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
727 756 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
728 757 assert_equal "duplicates", copied_relation.relation_type
729 758 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
730 759 assert_not_equal source_relation_cross_project.id, copied_relation.id
731 760 end
732 761
733 762 should "copy memberships" do
734 763 assert @project.valid?
735 764 assert @project.members.empty?
736 765 assert @project.copy(@source_project)
737 766
738 767 assert_equal @source_project.memberships.size, @project.memberships.size
739 768 @project.memberships.each do |membership|
740 769 assert membership
741 770 assert_equal @project, membership.project
742 771 end
743 772 end
744 773
745 774 should "copy project specific queries" do
746 775 assert @project.valid?
747 776 assert @project.queries.empty?
748 777 assert @project.copy(@source_project)
749 778
750 779 assert_equal @source_project.queries.size, @project.queries.size
751 780 @project.queries.each do |query|
752 781 assert query
753 782 assert_equal @project, query.project
754 783 end
755 784 end
756 785
757 786 should "copy versions" do
758 787 @source_project.versions << Version.generate!
759 788 @source_project.versions << Version.generate!
760 789
761 790 assert @project.versions.empty?
762 791 assert @project.copy(@source_project)
763 792
764 793 assert_equal @source_project.versions.size, @project.versions.size
765 794 @project.versions.each do |version|
766 795 assert version
767 796 assert_equal @project, version.project
768 797 end
769 798 end
770 799
771 800 should "copy wiki" do
772 801 assert_difference 'Wiki.count' do
773 802 assert @project.copy(@source_project)
774 803 end
775 804
776 805 assert @project.wiki
777 806 assert_not_equal @source_project.wiki, @project.wiki
778 807 assert_equal "Start page", @project.wiki.start_page
779 808 end
780 809
781 810 should "copy wiki pages and content with hierarchy" do
782 811 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
783 812 assert @project.copy(@source_project)
784 813 end
785 814
786 815 assert @project.wiki
787 816 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
788 817
789 818 @project.wiki.pages.each do |wiki_page|
790 819 assert wiki_page.content
791 820 assert !@source_project.wiki.pages.include?(wiki_page)
792 821 end
793 822
794 823 parent = @project.wiki.find_page('Parent_page')
795 824 child1 = @project.wiki.find_page('Child_page_1')
796 825 child2 = @project.wiki.find_page('Child_page_2')
797 826 assert_equal parent, child1.parent
798 827 assert_equal parent, child2.parent
799 828 end
800 829
801 830 should "copy issue categories" do
802 831 assert @project.copy(@source_project)
803 832
804 833 assert_equal 2, @project.issue_categories.size
805 834 @project.issue_categories.each do |issue_category|
806 835 assert !@source_project.issue_categories.include?(issue_category)
807 836 end
808 837 end
809 838
810 839 should "copy boards" do
811 840 assert @project.copy(@source_project)
812 841
813 842 assert_equal 1, @project.boards.size
814 843 @project.boards.each do |board|
815 844 assert !@source_project.boards.include?(board)
816 845 end
817 846 end
818 847
819 848 should "change the new issues to use the copied issue categories" do
820 849 issue = Issue.find(4)
821 850 issue.update_attribute(:category_id, 3)
822 851
823 852 assert @project.copy(@source_project)
824 853
825 854 @project.issues.each do |issue|
826 855 assert issue.category
827 856 assert_equal "Stock management", issue.category.name # Same name
828 857 assert_not_equal IssueCategory.find(3), issue.category # Different record
829 858 end
830 859 end
831 860
832 861 should "limit copy with :only option" do
833 862 assert @project.members.empty?
834 863 assert @project.issue_categories.empty?
835 864 assert @source_project.issues.any?
836 865
837 866 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
838 867
839 868 assert @project.members.any?
840 869 assert @project.issue_categories.any?
841 870 assert @project.issues.empty?
842 871 end
843 872
844 873 end
845 874
846 875 context "#start_date" do
847 876 setup do
848 877 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
849 878 @project = Project.generate!(:identifier => 'test0')
850 879 @project.trackers << Tracker.generate!
851 880 end
852 881
853 882 should "be nil if there are no issues on the project" do
854 883 assert_nil @project.start_date
855 884 end
856 885
857 886 should "be nil if issue tracking is disabled" do
858 887 Issue.generate_for_project!(@project, :start_date => Date.today)
859 888 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
860 889 @project.reload
861 890
862 891 assert_nil @project.start_date
863 892 end
864 893
865 894 should "be tested when issues have no start date"
866 895
867 896 should "be the earliest start date of it's issues" do
868 897 early = 7.days.ago.to_date
869 898 Issue.generate_for_project!(@project, :start_date => Date.today)
870 899 Issue.generate_for_project!(@project, :start_date => early)
871 900
872 901 assert_equal early, @project.start_date
873 902 end
874 903
875 904 end
876 905
877 906 context "#due_date" do
878 907 setup do
879 908 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
880 909 @project = Project.generate!(:identifier => 'test0')
881 910 @project.trackers << Tracker.generate!
882 911 end
883 912
884 913 should "be nil if there are no issues on the project" do
885 914 assert_nil @project.due_date
886 915 end
887 916
888 917 should "be nil if issue tracking is disabled" do
889 918 Issue.generate_for_project!(@project, :due_date => Date.today)
890 919 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
891 920 @project.reload
892 921
893 922 assert_nil @project.due_date
894 923 end
895 924
896 925 should "be tested when issues have no due date"
897 926
898 927 should "be the latest due date of it's issues" do
899 928 future = 7.days.from_now.to_date
900 929 Issue.generate_for_project!(@project, :due_date => future)
901 930 Issue.generate_for_project!(@project, :due_date => Date.today)
902 931
903 932 assert_equal future, @project.due_date
904 933 end
905 934
906 935 should "be the latest due date of it's versions" do
907 936 future = 7.days.from_now.to_date
908 937 @project.versions << Version.generate!(:effective_date => future)
909 938 @project.versions << Version.generate!(:effective_date => Date.today)
910 939
911 940
912 941 assert_equal future, @project.due_date
913 942
914 943 end
915 944
916 945 should "pick the latest date from it's issues and versions" do
917 946 future = 7.days.from_now.to_date
918 947 far_future = 14.days.from_now.to_date
919 948 Issue.generate_for_project!(@project, :due_date => far_future)
920 949 @project.versions << Version.generate!(:effective_date => future)
921 950
922 951 assert_equal far_future, @project.due_date
923 952 end
924 953
925 954 end
926 955
927 956 context "Project#completed_percent" do
928 957 setup do
929 958 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
930 959 @project = Project.generate!(:identifier => 'test0')
931 960 @project.trackers << Tracker.generate!
932 961 end
933 962
934 963 context "no versions" do
935 964 should "be 100" do
936 965 assert_equal 100, @project.completed_percent
937 966 end
938 967 end
939 968
940 969 context "with versions" do
941 970 should "return 0 if the versions have no issues" do
942 971 Version.generate!(:project => @project)
943 972 Version.generate!(:project => @project)
944 973
945 974 assert_equal 0, @project.completed_percent
946 975 end
947 976
948 977 should "return 100 if the version has only closed issues" do
949 978 v1 = Version.generate!(:project => @project)
950 979 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
951 980 v2 = Version.generate!(:project => @project)
952 981 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
953 982
954 983 assert_equal 100, @project.completed_percent
955 984 end
956 985
957 986 should "return the averaged completed percent of the versions (not weighted)" do
958 987 v1 = Version.generate!(:project => @project)
959 988 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
960 989 v2 = Version.generate!(:project => @project)
961 990 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
962 991
963 992 assert_equal 50, @project.completed_percent
964 993 end
965 994
966 995 end
967 996 end
968 997
969 998 context "#notified_users" do
970 999 setup do
971 1000 @project = Project.generate!
972 1001 @role = Role.generate!
973 1002
974 1003 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
975 1004 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
976 1005
977 1006 @all_events_user = User.generate!(:mail_notification => 'all')
978 1007 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
979 1008
980 1009 @no_events_user = User.generate!(:mail_notification => 'none')
981 1010 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
982 1011
983 1012 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
984 1013 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
985 1014
986 1015 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
987 1016 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
988 1017
989 1018 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
990 1019 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
991 1020 end
992 1021
993 1022 should "include members with a mail notification" do
994 1023 assert @project.notified_users.include?(@user_with_membership_notification)
995 1024 end
996 1025
997 1026 should "include users with the 'all' notification option" do
998 1027 assert @project.notified_users.include?(@all_events_user)
999 1028 end
1000 1029
1001 1030 should "not include users with the 'none' notification option" do
1002 1031 assert !@project.notified_users.include?(@no_events_user)
1003 1032 end
1004 1033
1005 1034 should "not include users with the 'only_my_events' notification option" do
1006 1035 assert !@project.notified_users.include?(@only_my_events_user)
1007 1036 end
1008 1037
1009 1038 should "not include users with the 'only_assigned' notification option" do
1010 1039 assert !@project.notified_users.include?(@only_assigned_user)
1011 1040 end
1012 1041
1013 1042 should "not include users with the 'only_owner' notification option" do
1014 1043 assert !@project.notified_users.include?(@only_owned_user)
1015 1044 end
1016 1045 end
1017 1046
1018 1047 end
General Comments 0
You need to be logged in to leave comments. Login now