##// END OF EJS Templates
Merged bug fixes r3412 to r3414....
Jean-Philippe Lang -
r3318:57dcbd73767a
parent child
Show More
@@ -1,61 +1,61
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_project, :authorize
20 20
21 21 def new
22 22 @relation = IssueRelation.new(params[:relation])
23 23 @relation.issue_from = @issue
24 if params[:relation] && !params[:relation][:issue_to_id].blank?
25 @relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id])
24 if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/)
25 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
26 26 end
27 27 @relation.save if request.post?
28 28 respond_to do |format|
29 29 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
30 30 format.js do
31 31 render :update do |page|
32 32 page.replace_html "relations", :partial => 'issues/relations'
33 33 if @relation.errors.empty?
34 34 page << "$('relation_delay').value = ''"
35 35 page << "$('relation_issue_to_id').value = ''"
36 36 end
37 37 end
38 38 end
39 39 end
40 40 end
41 41
42 42 def destroy
43 43 relation = IssueRelation.find(params[:id])
44 44 if request.post? && @issue.relations.include?(relation)
45 45 relation.destroy
46 46 @issue.reload
47 47 end
48 48 respond_to do |format|
49 49 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
50 50 format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
51 51 end
52 52 end
53 53
54 54 private
55 55 def find_project
56 56 @issue = Issue.find(params[:issue_id])
57 57 @project = @issue.project
58 58 rescue ActiveRecord::RecordNotFound
59 59 render_404
60 60 end
61 61 end
@@ -1,684 +1,696
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 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :memberships, :class_name => 'Member'
27 27 has_many :member_principals, :class_name => 'Member',
28 28 :include => :principal,
29 29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 30 has_many :users, :through => :members
31 31 has_many :principals, :through => :member_principals, :source => :principal
32 32
33 33 has_many :enabled_modules, :dependent => :delete_all
34 34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 36 has_many :issue_changes, :through => :issues, :source => :journals
37 37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 38 has_many :time_entries, :dependent => :delete_all
39 39 has_many :queries, :dependent => :delete_all
40 40 has_many :documents, :dependent => :destroy
41 41 has_many :news, :dependent => :delete_all, :include => :author
42 42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 44 has_one :repository, :dependent => :destroy
45 45 has_many :changesets, :through => :repository
46 46 has_one :wiki, :dependent => :destroy
47 47 # Custom field for the project issues
48 48 has_and_belongs_to_many :issue_custom_fields,
49 49 :class_name => 'IssueCustomField',
50 50 :order => "#{CustomField.table_name}.position",
51 51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 52 :association_foreign_key => 'custom_field_id'
53 53
54 54 acts_as_nested_set :order => 'name'
55 55 acts_as_attachable :view_permission => :view_files,
56 56 :delete_permission => :manage_files
57 57
58 58 acts_as_customizable
59 59 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
60 60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
62 62 :author => nil
63 63
64 64 attr_protected :status, :enabled_module_names
65 65
66 66 validates_presence_of :name, :identifier
67 67 validates_uniqueness_of :name, :identifier
68 68 validates_associated :repository, :wiki
69 69 validates_length_of :name, :maximum => 30
70 70 validates_length_of :homepage, :maximum => 255
71 71 validates_length_of :identifier, :in => 1..20
72 72 # donwcase letters, digits, dashes but not digits only
73 73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 74 # reserved words
75 75 validates_exclusion_of :identifier, :in => %w( new )
76 76
77 77 before_destroy :delete_all_members, :destroy_children
78 78
79 79 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] } }
80 80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 81 named_scope :all_public, { :conditions => { :is_public => true } }
82 82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83 83
84 84 def identifier=(identifier)
85 85 super unless identifier_frozen?
86 86 end
87 87
88 88 def identifier_frozen?
89 89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 90 end
91 91
92 92 # returns latest created projects
93 93 # non public projects will be returned only if user is a member of those
94 94 def self.latest(user=nil, count=5)
95 95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 96 end
97 97
98 98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 99 #
100 100 # Examples:
101 101 # Projects.visible_by(admin) => "projects.status = 1"
102 102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 103 def self.visible_by(user=nil)
104 104 user ||= User.current
105 105 if user && user.admin?
106 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 107 elsif user && user.memberships.any?
108 108 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(',')}))"
109 109 else
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 111 end
112 112 end
113 113
114 114 def self.allowed_to_condition(user, permission, options={})
115 115 statements = []
116 116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 117 if perm = Redmine::AccessControl.permission(permission)
118 118 unless perm.project_module.nil?
119 119 # If the permission belongs to a project module, make sure the module is enabled
120 120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 121 end
122 122 end
123 123 if options[:project]
124 124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 127 end
128 128 if user.admin?
129 129 # no restriction
130 130 else
131 131 statements << "1=0"
132 132 if user.logged?
133 133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 135 end
136 136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 138 else
139 139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 140 # anonymous user allowed on public project
141 141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 142 end
143 143 end
144 144 end
145 145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 146 end
147 147
148 148 # Returns the Systemwide and project specific activities
149 149 def activities(include_inactive=false)
150 150 if include_inactive
151 151 return all_activities
152 152 else
153 153 return active_activities
154 154 end
155 155 end
156 156
157 157 # Will create a new Project specific Activity or update an existing one
158 158 #
159 159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 160 # does not successfully save.
161 161 def update_or_create_time_entry_activity(id, activity_hash)
162 162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 163 self.create_time_entry_activity_if_needed(activity_hash)
164 164 else
165 165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 166 activity.update_attributes(activity_hash) if activity
167 167 end
168 168 end
169 169
170 170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 171 #
172 172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 173 # does not successfully save.
174 174 def create_time_entry_activity_if_needed(activity)
175 175 if activity['parent_id']
176 176
177 177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 178 activity['name'] = parent_activity.name
179 179 activity['position'] = parent_activity.position
180 180
181 181 if Enumeration.overridding_change?(activity, parent_activity)
182 182 project_activity = self.time_entry_activities.create(activity)
183 183
184 184 if project_activity.new_record?
185 185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 186 else
187 187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 188 end
189 189 end
190 190 end
191 191 end
192 192
193 193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 194 #
195 195 # Examples:
196 196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 197 # project.project_condition(false) => "projects.id = 1"
198 198 def project_condition(with_subprojects)
199 199 cond = "#{Project.table_name}.id = #{id}"
200 200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 201 cond
202 202 end
203 203
204 204 def self.find(*args)
205 205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 206 project = find_by_identifier(*args)
207 207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 208 project
209 209 else
210 210 super
211 211 end
212 212 end
213 213
214 214 def to_param
215 215 # id is used for projects with a numeric identifier (compatibility)
216 216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 217 end
218 218
219 219 def active?
220 220 self.status == STATUS_ACTIVE
221 221 end
222 222
223 223 # Archives the project and its descendants
224 224 def archive
225 225 # Check that there is no issue of a non descendant project that is assigned
226 226 # to one of the project or descendant versions
227 227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 228 if v_ids.any? && Issue.find(:first, :include => :project,
229 229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 231 return false
232 232 end
233 233 Project.transaction do
234 234 archive!
235 235 end
236 236 true
237 237 end
238 238
239 239 # Unarchives the project
240 240 # All its ancestors must be active
241 241 def unarchive
242 242 return false if ancestors.detect {|a| !a.active?}
243 243 update_attribute :status, STATUS_ACTIVE
244 244 end
245 245
246 246 # Returns an array of projects the project can be moved to
247 247 # by the current user
248 248 def allowed_parents
249 249 return @allowed_parents if @allowed_parents
250 250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 251 @allowed_parents = @allowed_parents - self_and_descendants
252 252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 253 @allowed_parents << nil
254 254 end
255 255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 256 @allowed_parents << parent
257 257 end
258 258 @allowed_parents
259 259 end
260 260
261 261 # Sets the parent of the project with authorization check
262 262 def set_allowed_parent!(p)
263 263 unless p.nil? || p.is_a?(Project)
264 264 if p.to_s.blank?
265 265 p = nil
266 266 else
267 267 p = Project.find_by_id(p)
268 268 return false unless p
269 269 end
270 270 end
271 271 if p.nil?
272 272 if !new_record? && allowed_parents.empty?
273 273 return false
274 274 end
275 275 elsif !allowed_parents.include?(p)
276 276 return false
277 277 end
278 278 set_parent!(p)
279 279 end
280 280
281 281 # Sets the parent of the project
282 282 # Argument can be either a Project, a String, a Fixnum or nil
283 283 def set_parent!(p)
284 284 unless p.nil? || p.is_a?(Project)
285 285 if p.to_s.blank?
286 286 p = nil
287 287 else
288 288 p = Project.find_by_id(p)
289 289 return false unless p
290 290 end
291 291 end
292 292 if p == parent && !p.nil?
293 293 # Nothing to do
294 294 true
295 295 elsif p.nil? || (p.active? && move_possible?(p))
296 296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 297 sibs = (p.nil? ? self.class.roots : p.children)
298 298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 299 if to_be_inserted_before
300 300 move_to_left_of(to_be_inserted_before)
301 301 elsif p.nil?
302 302 if sibs.empty?
303 303 # move_to_root adds the project in first (ie. left) position
304 304 move_to_root
305 305 else
306 306 move_to_right_of(sibs.last) unless self == sibs.last
307 307 end
308 308 else
309 309 # move_to_child_of adds the project in last (ie.right) position
310 310 move_to_child_of(p)
311 311 end
312 312 Issue.update_versions_from_hierarchy_change(self)
313 313 true
314 314 else
315 315 # Can not move to the given target
316 316 false
317 317 end
318 318 end
319 319
320 320 # Returns an array of the trackers used by the project and its active sub projects
321 321 def rolled_up_trackers
322 322 @rolled_up_trackers ||=
323 323 Tracker.find(:all, :include => :projects,
324 324 :select => "DISTINCT #{Tracker.table_name}.*",
325 325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 326 :order => "#{Tracker.table_name}.position")
327 327 end
328 328
329 329 # Closes open and locked project versions that are completed
330 330 def close_completed_versions
331 331 Version.transaction do
332 332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 333 if version.completed?
334 334 version.update_attribute(:status, 'closed')
335 335 end
336 336 end
337 337 end
338 338 end
339 339
340 340 # Returns a scope of the Versions used by the project
341 341 def shared_versions
342 342 @shared_versions ||=
343 343 Version.scoped(:include => :project,
344 344 :conditions => "#{Project.table_name}.id = #{id}" +
345 345 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
346 346 " #{Version.table_name}.sharing = 'system'" +
347 347 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
348 348 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
349 349 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
350 350 "))")
351 351 end
352 352
353 353 # Returns a hash of project users grouped by role
354 354 def users_by_role
355 355 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
356 356 m.roles.each do |r|
357 357 h[r] ||= []
358 358 h[r] << m.user
359 359 end
360 360 h
361 361 end
362 362 end
363 363
364 364 # Deletes all project's members
365 365 def delete_all_members
366 366 me, mr = Member.table_name, MemberRole.table_name
367 367 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
368 368 Member.delete_all(['project_id = ?', id])
369 369 end
370 370
371 371 # Users issues can be assigned to
372 372 def assignable_users
373 373 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
374 374 end
375 375
376 376 # Returns the mail adresses of users that should be always notified on project events
377 377 def recipients
378 378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
379 379 end
380 380
381 381 # Returns the users that should be notified on project events
382 382 def notified_users
383 383 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
384 384 end
385 385
386 386 # Returns an array of all custom fields enabled for project issues
387 387 # (explictly associated custom fields and custom fields enabled for all projects)
388 388 def all_issue_custom_fields
389 389 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
390 390 end
391 391
392 392 def project
393 393 self
394 394 end
395 395
396 396 def <=>(project)
397 397 name.downcase <=> project.name.downcase
398 398 end
399 399
400 400 def to_s
401 401 name
402 402 end
403 403
404 404 # Returns a short description of the projects (first lines)
405 405 def short_description(length = 255)
406 406 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
407 407 end
408 408
409 409 # Return true if this project is allowed to do the specified action.
410 410 # action can be:
411 411 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
412 412 # * a permission Symbol (eg. :edit_project)
413 413 def allows_to?(action)
414 414 if action.is_a? Hash
415 415 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
416 416 else
417 417 allowed_permissions.include? action
418 418 end
419 419 end
420 420
421 421 def module_enabled?(module_name)
422 422 module_name = module_name.to_s
423 423 enabled_modules.detect {|m| m.name == module_name}
424 424 end
425 425
426 426 def enabled_module_names=(module_names)
427 427 if module_names && module_names.is_a?(Array)
428 428 module_names = module_names.collect(&:to_s)
429 429 # remove disabled modules
430 430 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
431 431 # add new modules
432 432 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
433 433 else
434 434 enabled_modules.clear
435 435 end
436 436 end
437 437
438 438 # Returns an auto-generated project identifier based on the last identifier used
439 439 def self.next_identifier
440 440 p = Project.find(:first, :order => 'created_on DESC')
441 441 p.nil? ? nil : p.identifier.to_s.succ
442 442 end
443 443
444 444 # Copies and saves the Project instance based on the +project+.
445 445 # Duplicates the source project's:
446 446 # * Wiki
447 447 # * Versions
448 448 # * Categories
449 449 # * Issues
450 450 # * Members
451 451 # * Queries
452 452 #
453 453 # Accepts an +options+ argument to specify what to copy
454 454 #
455 455 # Examples:
456 456 # project.copy(1) # => copies everything
457 457 # project.copy(1, :only => 'members') # => copies members only
458 458 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
459 459 def copy(project, options={})
460 460 project = project.is_a?(Project) ? project : Project.find(project)
461 461
462 462 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
463 463 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
464 464
465 465 Project.transaction do
466 466 if save
467 467 reload
468 468 to_be_copied.each do |name|
469 469 send "copy_#{name}", project
470 470 end
471 471 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
472 472 save
473 473 end
474 474 end
475 475 end
476 476
477 477
478 478 # Copies +project+ and returns the new instance. This will not save
479 479 # the copy
480 480 def self.copy_from(project)
481 481 begin
482 482 project = project.is_a?(Project) ? project : Project.find(project)
483 483 if project
484 484 # clear unique attributes
485 485 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
486 486 copy = Project.new(attributes)
487 487 copy.enabled_modules = project.enabled_modules
488 488 copy.trackers = project.trackers
489 489 copy.custom_values = project.custom_values.collect {|v| v.clone}
490 490 copy.issue_custom_fields = project.issue_custom_fields
491 491 return copy
492 492 else
493 493 return nil
494 494 end
495 495 rescue ActiveRecord::RecordNotFound
496 496 return nil
497 497 end
498 498 end
499 499
500 500 private
501 501
502 502 # Destroys children before destroying self
503 503 def destroy_children
504 504 children.each do |child|
505 505 child.destroy
506 506 end
507 507 end
508 508
509 509 # Copies wiki from +project+
510 510 def copy_wiki(project)
511 511 # Check that the source project has a wiki first
512 512 unless project.wiki.nil?
513 513 self.wiki ||= Wiki.new
514 514 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
515 wiki_pages_map = {}
515 516 project.wiki.pages.each do |page|
517 # Skip pages without content
518 next if page.content.nil?
516 519 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
517 520 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
518 521 new_wiki_page.content = new_wiki_content
519 522 wiki.pages << new_wiki_page
523 wiki_pages_map[page.id] = new_wiki_page
524 end
525 wiki.save
526 # Reproduce page hierarchy
527 project.wiki.pages.each do |page|
528 if page.parent_id && wiki_pages_map[page.id]
529 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
530 wiki_pages_map[page.id].save
531 end
520 532 end
521 533 end
522 534 end
523 535
524 536 # Copies versions from +project+
525 537 def copy_versions(project)
526 538 project.versions.each do |version|
527 539 new_version = Version.new
528 540 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
529 541 self.versions << new_version
530 542 end
531 543 end
532 544
533 545 # Copies issue categories from +project+
534 546 def copy_issue_categories(project)
535 547 project.issue_categories.each do |issue_category|
536 548 new_issue_category = IssueCategory.new
537 549 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
538 550 self.issue_categories << new_issue_category
539 551 end
540 552 end
541 553
542 554 # Copies issues from +project+
543 555 def copy_issues(project)
544 556 # Stores the source issue id as a key and the copied issues as the
545 557 # value. Used to map the two togeather for issue relations.
546 558 issues_map = {}
547 559
548 560 project.issues.each do |issue|
549 561 new_issue = Issue.new
550 562 new_issue.copy_from(issue)
551 563 # Reassign fixed_versions by name, since names are unique per
552 564 # project and the versions for self are not yet saved
553 565 if issue.fixed_version
554 566 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
555 567 end
556 568 # Reassign the category by name, since names are unique per
557 569 # project and the categories for self are not yet saved
558 570 if issue.category
559 571 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
560 572 end
561 573 self.issues << new_issue
562 574 issues_map[issue.id] = new_issue
563 575 end
564 576
565 577 # Relations after in case issues related each other
566 578 project.issues.each do |issue|
567 579 new_issue = issues_map[issue.id]
568 580
569 581 # Relations
570 582 issue.relations_from.each do |source_relation|
571 583 new_issue_relation = IssueRelation.new
572 584 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
573 585 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
574 586 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
575 587 new_issue_relation.issue_to = source_relation.issue_to
576 588 end
577 589 new_issue.relations_from << new_issue_relation
578 590 end
579 591
580 592 issue.relations_to.each do |source_relation|
581 593 new_issue_relation = IssueRelation.new
582 594 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
583 595 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
584 596 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
585 597 new_issue_relation.issue_from = source_relation.issue_from
586 598 end
587 599 new_issue.relations_to << new_issue_relation
588 600 end
589 601 end
590 602 end
591 603
592 604 # Copies members from +project+
593 605 def copy_members(project)
594 606 project.memberships.each do |member|
595 607 new_member = Member.new
596 608 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
597 609 # only copy non inherited roles
598 610 # inherited roles will be added when copying the group membership
599 611 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
600 612 next if role_ids.empty?
601 613 new_member.role_ids = role_ids
602 614 new_member.project = self
603 615 self.members << new_member
604 616 end
605 617 end
606 618
607 619 # Copies queries from +project+
608 620 def copy_queries(project)
609 621 project.queries.each do |query|
610 622 new_query = Query.new
611 623 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
612 624 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
613 625 new_query.project = self
614 626 self.queries << new_query
615 627 end
616 628 end
617 629
618 630 # Copies boards from +project+
619 631 def copy_boards(project)
620 632 project.boards.each do |board|
621 633 new_board = Board.new
622 634 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
623 635 new_board.project = self
624 636 self.boards << new_board
625 637 end
626 638 end
627 639
628 640 def allowed_permissions
629 641 @allowed_permissions ||= begin
630 642 module_names = enabled_modules.collect {|m| m.name}
631 643 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
632 644 end
633 645 end
634 646
635 647 def allowed_actions
636 648 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
637 649 end
638 650
639 651 # Returns all the active Systemwide and project specific activities
640 652 def active_activities
641 653 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
642 654
643 655 if overridden_activity_ids.empty?
644 656 return TimeEntryActivity.shared.active
645 657 else
646 658 return system_activities_and_project_overrides
647 659 end
648 660 end
649 661
650 662 # Returns all the Systemwide and project specific activities
651 663 # (inactive and active)
652 664 def all_activities
653 665 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
654 666
655 667 if overridden_activity_ids.empty?
656 668 return TimeEntryActivity.shared
657 669 else
658 670 return system_activities_and_project_overrides(true)
659 671 end
660 672 end
661 673
662 674 # Returns the systemwide active activities merged with the project specific overrides
663 675 def system_activities_and_project_overrides(include_inactive=false)
664 676 if include_inactive
665 677 return TimeEntryActivity.shared.
666 678 find(:all,
667 679 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
668 680 self.time_entry_activities
669 681 else
670 682 return TimeEntryActivity.shared.active.
671 683 find(:all,
672 684 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
673 685 self.time_entry_activities.active
674 686 end
675 687 end
676 688
677 689 # Archives subprojects recursively
678 690 def archive!
679 691 children.each do |subproject|
680 692 subproject.send :archive!
681 693 end
682 694 update_attribute :status, STATUS_ARCHIVED
683 695 end
684 696 end
@@ -1,852 +1,853
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #header h1 a.ancestor { font-size: 80%; }
29 29 #quick-search {float:right;}
30 30
31 31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 32 #main-menu ul {margin: 0; padding: 0;}
33 33 #main-menu li {
34 34 float:left;
35 35 list-style-type:none;
36 36 margin: 0px 2px 0px 0px;
37 37 padding: 0px 0px 0px 0px;
38 38 white-space:nowrap;
39 39 }
40 40 #main-menu li a {
41 41 display: block;
42 42 color: #fff;
43 43 text-decoration: none;
44 44 font-weight: bold;
45 45 margin: 0;
46 46 padding: 4px 10px 4px 10px;
47 47 }
48 48 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 50
51 51 #admin-menu ul {margin: 0; padding: 0;}
52 52 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
53 53
54 54 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
55 55 #admin-menu a.projects { background-image: url(../images/projects.png); }
56 56 #admin-menu a.users { background-image: url(../images/user.png); }
57 57 #admin-menu a.groups { background-image: url(../images/group.png); }
58 58 #admin-menu a.roles { background-image: url(../images/database_key.png); }
59 59 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
60 60 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
61 61 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
62 62 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
63 63 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
64 64 #admin-menu a.settings { background-image: url(../images/changeset.png); }
65 65 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
66 66 #admin-menu a.info { background-image: url(../images/help.png); }
67 67
68 68 #main {background-color:#EEEEEE;}
69 69
70 70 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
71 71 * html #sidebar{ width: 17%; }
72 72 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
73 73 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
74 74 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
75 75
76 76 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
77 77 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
78 78 html>body #content { min-height: 600px; }
79 79 * html body #content { height: 600px; } /* IE */
80 80
81 81 #main.nosidebar #sidebar{ display: none; }
82 82 #main.nosidebar #content{ width: auto; border-right: 0; }
83 83
84 84 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
85 85
86 86 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
87 87 #login-form table td {padding: 6px;}
88 88 #login-form label {font-weight: bold;}
89 89 #login-form input#username, #login-form input#password { width: 300px; }
90 90
91 91 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
92 92
93 93 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
94 94
95 95 /***** Links *****/
96 96 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
97 97 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
98 98 a img{ border: 0; }
99 99
100 100 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
101 101
102 102 /***** Tables *****/
103 103 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
104 104 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
105 105 table.list td { vertical-align: top; }
106 106 table.list td.id { width: 2%; text-align: center;}
107 107 table.list td.checkbox { width: 15px; padding: 0px;}
108 108 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
109 109 table.list td.buttons a { padding-right: 0.6em; }
110 110
111 111 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
112 112 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
113 113
114 114 tr.issue { text-align: center; white-space: nowrap; }
115 115 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
116 116 tr.issue td.subject { text-align: left; }
117 117 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
118 118
119 119 tr.entry { border: 1px solid #f8f8f8; }
120 120 tr.entry td { white-space: nowrap; }
121 121 tr.entry td.filename { width: 30%; }
122 122 tr.entry td.size { text-align: right; font-size: 90%; }
123 123 tr.entry td.revision, tr.entry td.author { text-align: center; }
124 124 tr.entry td.age { text-align: right; }
125 125 tr.entry.file td.filename a { margin-left: 16px; }
126 126
127 127 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
128 128 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
129 129
130 130 tr.changeset td.author { text-align: center; width: 15%; }
131 131 tr.changeset td.committed_on { text-align: center; width: 15%; }
132 132
133 133 table.files tr.file td { text-align: center; }
134 134 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
135 135 table.files tr.file td.digest { font-size: 80%; }
136 136
137 137 table.members td.roles, table.memberships td.roles { width: 45%; }
138 138
139 139 tr.message { height: 2.6em; }
140 140 tr.message td.last_message { font-size: 80%; }
141 141 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
142 142 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
143 143
144 144 tr.version.closed, tr.version.closed a { color: #999; }
145 145 tr.version td.name { padding-left: 20px; }
146 146 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
147 147 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
148 148
149 149 tr.user td { width:13%; }
150 150 tr.user td.email { width:18%; }
151 151 tr.user td { white-space: nowrap; }
152 152 tr.user.locked, tr.user.registered { color: #aaa; }
153 153 tr.user.locked a, tr.user.registered a { color: #aaa; }
154 154
155 155 tr.time-entry { text-align: center; white-space: nowrap; }
156 156 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
157 157 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
158 158 td.hours .hours-dec { font-size: 0.9em; }
159 159
160 160 table.plugins td { vertical-align: middle; }
161 161 table.plugins td.configure { text-align: right; padding-right: 1em; }
162 162 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
163 163 table.plugins span.description { display: block; font-size: 0.9em; }
164 164 table.plugins span.url { display: block; font-size: 0.9em; }
165 165
166 166 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
167 167 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
168 168
169 169 table.list tbody tr:hover { background-color:#ffffdd; }
170 170 table.list tbody tr.group:hover { background-color:inherit; }
171 171 table td {padding:2px;}
172 172 table p {margin:0;}
173 173 .odd {background-color:#f6f7f8;}
174 174 .even {background-color: #fff;}
175 175
176 176 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
177 177 a.sort.asc { background-image: url(../images/sort_asc.png); }
178 178 a.sort.desc { background-image: url(../images/sort_desc.png); }
179 179
180 180 table.attributes { width: 100% }
181 181 table.attributes th { vertical-align: top; text-align: left; }
182 182 table.attributes td { vertical-align: top; }
183 183
184 184 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
185 185
186 186 td.center {text-align:center;}
187 187
188 188 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
189 189
190 190 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
191 191 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
192 192 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
193 193 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
194 194
195 195 .highlight { background-color: #FCFD8D;}
196 196 .highlight.token-1 { background-color: #faa;}
197 197 .highlight.token-2 { background-color: #afa;}
198 198 .highlight.token-3 { background-color: #aaf;}
199 199
200 200 .box{
201 201 padding:6px;
202 202 margin-bottom: 10px;
203 203 background-color:#f6f6f6;
204 204 color:#505050;
205 205 line-height:1.5em;
206 206 border: 1px solid #e4e4e4;
207 207 }
208 208
209 209 div.square {
210 210 border: 1px solid #999;
211 211 float: left;
212 212 margin: .3em .4em 0 .4em;
213 213 overflow: hidden;
214 214 width: .6em; height: .6em;
215 215 }
216 216 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
217 217 .contextual input, .contextual select {font-size:0.9em;}
218 218 .message .contextual { margin-top: 0; }
219 219
220 220 .splitcontentleft{float:left; width:49%;}
221 221 .splitcontentright{float:right; width:49%;}
222 222 form {display: inline;}
223 223 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
224 224 fieldset {border: 1px solid #e4e4e4; margin:0;}
225 225 legend {color: #484848;}
226 226 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
227 227 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
228 228 blockquote blockquote { margin-left: 0;}
229 229 acronym { border-bottom: 1px dotted; cursor: help; }
230 230 textarea.wiki-edit { width: 99%; }
231 231 li p {margin-top: 0;}
232 232 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
233 233 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
234 234 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
235 235 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
236 236
237 237 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
238 238 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
239 239 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
240 240
241 241 fieldset#date-range p { margin: 2px 0 2px 0; }
242 242 fieldset#filters table { border-collapse: collapse; }
243 243 fieldset#filters table td { padding: 0; vertical-align: middle; }
244 244 fieldset#filters tr.filter { height: 2em; }
245 245 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
246 246 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
247 247
248 248 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
249 249 div#issue-changesets .changeset { padding: 4px;}
250 250 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
251 251 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
252 252
253 253 div#activity dl, #search-results { margin-left: 2em; }
254 254 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
255 255 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
256 256 div#activity dt.me .time { border-bottom: 1px solid #999; }
257 257 div#activity dt .time { color: #777; font-size: 80%; }
258 258 div#activity dd .description, #search-results dd .description { font-style: italic; }
259 259 div#activity span.project:after, #search-results span.project:after { content: " -"; }
260 260 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
261 261
262 262 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
263 263
264 264 div#search-results-counts {float:right;}
265 265 div#search-results-counts ul { margin-top: 0.5em; }
266 266 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
267 267
268 268 dt.issue { background-image: url(../images/ticket.png); }
269 269 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
270 270 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
271 271 dt.issue-note { background-image: url(../images/ticket_note.png); }
272 272 dt.changeset { background-image: url(../images/changeset.png); }
273 273 dt.news { background-image: url(../images/news.png); }
274 274 dt.message { background-image: url(../images/message.png); }
275 275 dt.reply { background-image: url(../images/comments.png); }
276 276 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
277 277 dt.attachment { background-image: url(../images/attachment.png); }
278 278 dt.document { background-image: url(../images/document.png); }
279 279 dt.project { background-image: url(../images/projects.png); }
280 280 dt.time-entry { background-image: url(../images/time.png); }
281 281
282 282 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
283 283
284 284 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
285 285 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
286 286 div#roadmap .wiki h1:first-child { display: none; }
287 287 div#roadmap .wiki h1 { font-size: 120%; }
288 288 div#roadmap .wiki h2 { font-size: 110%; }
289 289
290 290 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
291 291 div#version-summary fieldset { margin-bottom: 1em; }
292 292 div#version-summary .total-hours { text-align: right; }
293 293
294 294 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
295 295 table#time-report tbody tr { font-style: italic; color: #777; }
296 296 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
297 297 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
298 298 table#time-report .hours-dec { font-size: 0.9em; }
299 299
300 300 form#issue-form .attributes { margin-bottom: 8px; }
301 301 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
302 302 form#issue-form .attributes select { min-width: 30%; }
303 303
304 304 ul.projects { margin: 0; padding-left: 1em; }
305 305 ul.projects.root { margin: 0; padding: 0; }
306 306 ul.projects ul { border-left: 3px solid #e0e0e0; }
307 307 ul.projects li { list-style-type:none; }
308 308 ul.projects li.root { margin-bottom: 1em; }
309 309 ul.projects li.child { margin-top: 1em;}
310 310 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
311 311 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
312 312
313 313 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
314 314 #tracker_project_ids li { list-style-type:none; }
315 315
316 316 ul.properties {padding:0; font-size: 0.9em; color: #777;}
317 317 ul.properties li {list-style-type:none;}
318 318 ul.properties li span {font-style:italic;}
319 319
320 320 .total-hours { font-size: 110%; font-weight: bold; }
321 321 .total-hours span.hours-int { font-size: 120%; }
322 322
323 323 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
324 324 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
325 325
326 326 #workflow_copy_form select { width: 200px; }
327 327
328 328 .pagination {font-size: 90%}
329 329 p.pagination {margin-top:8px;}
330 330
331 331 /***** Tabular forms ******/
332 332 .tabular p{
333 333 margin: 0;
334 334 padding: 5px 0 8px 0;
335 335 padding-left: 180px; /*width of left column containing the label elements*/
336 336 height: 1%;
337 337 clear:left;
338 338 }
339 339
340 340 html>body .tabular p {overflow:hidden;}
341 341
342 342 .tabular label{
343 343 font-weight: bold;
344 344 float: left;
345 345 text-align: right;
346 346 margin-left: -180px; /*width of left column*/
347 347 width: 175px; /*width of labels. Should be smaller than left column to create some right
348 348 margin*/
349 349 }
350 350
351 351 .tabular label.floating{
352 352 font-weight: normal;
353 353 margin-left: 0px;
354 354 text-align: left;
355 355 width: 270px;
356 356 }
357 357
358 358 .tabular label.block{
359 359 font-weight: normal;
360 360 margin-left: 0px !important;
361 361 text-align: left;
362 362 float: none;
363 363 display: block;
364 364 width: auto;
365 365 }
366 366
367 367 input#time_entry_comments { width: 90%;}
368 368
369 369 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
370 370
371 371 .tabular.settings p{ padding-left: 300px; }
372 372 .tabular.settings label{ margin-left: -300px; width: 295px; }
373 373 .tabular.settings textarea { width: 99%; }
374 374
375 375 fieldset.settings label { display: block; }
376 376
377 377 .required {color: #bb0000;}
378 378 .summary {font-style: italic;}
379 379
380 380 #attachments_fields input[type=text] {margin-left: 8px; }
381 381
382 382 div.attachments { margin-top: 12px; }
383 383 div.attachments p { margin:4px 0 2px 0; }
384 384 div.attachments img { vertical-align: middle; }
385 385 div.attachments span.author { font-size: 0.9em; color: #888; }
386 386
387 387 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
388 388 .other-formats span + span:before { content: "| "; }
389 389
390 390 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
391 391
392 392 /* Project members tab */
393 393 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
394 394 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
395 395 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
396 396 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
397 397 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
398 398 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
399 399
400 400 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
401 401
402 402 * html div#tab-content-members fieldset div { height: 450px; }
403 403
404 404 /***** Flash & error messages ****/
405 405 #errorExplanation, div.flash, .nodata, .warning {
406 406 padding: 4px 4px 4px 30px;
407 407 margin-bottom: 12px;
408 408 font-size: 1.1em;
409 409 border: 2px solid;
410 410 }
411 411
412 412 div.flash {margin-top: 8px;}
413 413
414 414 div.flash.error, #errorExplanation {
415 415 background: url(../images/exclamation.png) 8px 50% no-repeat;
416 416 background-color: #ffe3e3;
417 417 border-color: #dd0000;
418 418 color: #880000;
419 419 }
420 420
421 421 div.flash.notice {
422 422 background: url(../images/true.png) 8px 5px no-repeat;
423 423 background-color: #dfffdf;
424 424 border-color: #9fcf9f;
425 425 color: #005f00;
426 426 }
427 427
428 428 div.flash.warning {
429 429 background: url(../images/warning.png) 8px 5px no-repeat;
430 430 background-color: #FFEBC1;
431 431 border-color: #FDBF3B;
432 432 color: #A6750C;
433 433 text-align: left;
434 434 }
435 435
436 436 .nodata, .warning {
437 437 text-align: center;
438 438 background-color: #FFEBC1;
439 439 border-color: #FDBF3B;
440 440 color: #A6750C;
441 441 }
442 442
443 443 #errorExplanation ul { font-size: 0.9em;}
444 444 #errorExplanation h2, #errorExplanation p { display: none; }
445 445
446 446 /***** Ajax indicator ******/
447 447 #ajax-indicator {
448 448 position: absolute; /* fixed not supported by IE */
449 449 background-color:#eee;
450 450 border: 1px solid #bbb;
451 451 top:35%;
452 452 left:40%;
453 453 width:20%;
454 454 font-weight:bold;
455 455 text-align:center;
456 456 padding:0.6em;
457 457 z-index:100;
458 458 filter:alpha(opacity=50);
459 459 opacity: 0.5;
460 460 }
461 461
462 462 html>body #ajax-indicator { position: fixed; }
463 463
464 464 #ajax-indicator span {
465 465 background-position: 0% 40%;
466 466 background-repeat: no-repeat;
467 467 background-image: url(../images/loading.gif);
468 468 padding-left: 26px;
469 469 vertical-align: bottom;
470 470 }
471 471
472 472 /***** Calendar *****/
473 473 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
474 474 table.cal thead th {width: 14%;}
475 475 table.cal tbody tr {height: 100px;}
476 476 table.cal th { background-color:#EEEEEE; padding: 4px; }
477 477 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
478 478 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
479 479 table.cal td.odd p.day-num {color: #bbb;}
480 480 table.cal td.today {background:#ffffdd;}
481 481 table.cal td.today p.day-num {font-weight: bold;}
482 482
483 483 /***** Tooltips ******/
484 484 .tooltip{position:relative;z-index:24;}
485 485 .tooltip:hover{z-index:25;color:#000;}
486 486 .tooltip span.tip{display: none; text-align:left;}
487 487
488 488 div.tooltip:hover span.tip{
489 489 display:block;
490 490 position:absolute;
491 491 top:12px; left:24px; width:270px;
492 492 border:1px solid #555;
493 493 background-color:#fff;
494 494 padding: 4px;
495 495 font-size: 0.8em;
496 496 color:#505050;
497 497 }
498 498
499 499 /***** Progress bar *****/
500 500 table.progress {
501 501 border: 1px solid #D7D7D7;
502 502 border-collapse: collapse;
503 503 border-spacing: 0pt;
504 504 empty-cells: show;
505 505 text-align: center;
506 506 float:left;
507 507 margin: 1px 6px 1px 0px;
508 508 }
509 509
510 510 table.progress td { height: 0.9em; }
511 511 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
512 512 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
513 513 table.progress td.open { background: #FFF none repeat scroll 0%; }
514 514 p.pourcent {font-size: 80%;}
515 515 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
516 516
517 517 /***** Tabs *****/
518 518 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
519 519 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
520 520 #content .tabs ul li {
521 521 float:left;
522 522 list-style-type:none;
523 523 white-space:nowrap;
524 524 margin-right:8px;
525 525 background:#fff;
526 526 position:relative;
527 527 margin-bottom:-1px;
528 528 }
529 529 #content .tabs ul li a{
530 530 display:block;
531 531 font-size: 0.9em;
532 532 text-decoration:none;
533 533 line-height:1.3em;
534 534 padding:4px 6px 4px 6px;
535 535 border: 1px solid #ccc;
536 536 border-bottom: 1px solid #bbbbbb;
537 537 background-color: #eeeeee;
538 538 color:#777;
539 539 font-weight:bold;
540 540 }
541 541
542 542 #content .tabs ul li a:hover {
543 543 background-color: #ffffdd;
544 544 text-decoration:none;
545 545 }
546 546
547 547 #content .tabs ul li a.selected {
548 548 background-color: #fff;
549 549 border: 1px solid #bbbbbb;
550 550 border-bottom: 1px solid #fff;
551 551 }
552 552
553 553 #content .tabs ul li a.selected:hover {
554 554 background-color: #fff;
555 555 }
556 556
557 557 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
558 558
559 559 button.tab-left, button.tab-right {
560 560 font-size: 0.9em;
561 561 cursor: pointer;
562 562 height:24px;
563 563 border: 1px solid #ccc;
564 564 border-bottom: 1px solid #bbbbbb;
565 565 position:absolute;
566 566 padding:4px;
567 567 width: 20px;
568 568 bottom: -1px;
569 569 }
570 570
571 571 button.tab-left {
572 572 right: 20px;
573 573 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
574 574 }
575 575
576 576 button.tab-right {
577 577 right: 0;
578 578 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
579 579 }
580 580
581 581 /***** Auto-complete *****/
582 582 div.autocomplete {
583 583 position:absolute;
584 584 width:250px;
585 585 background-color:white;
586 586 margin:0;
587 587 padding:0;
588 588 }
589 589 div.autocomplete ul {
590 590 list-style-type:none;
591 591 margin:0;
592 592 padding:0;
593 593 }
594 594 div.autocomplete ul li.selected { background-color: #ffb;}
595 595 div.autocomplete ul li {
596 596 list-style-type:none;
597 597 display:block;
598 598 margin:0;
599 599 padding:2px;
600 600 cursor:pointer;
601 601 font-size: 90%;
602 602 border-bottom: 1px solid #ccc;
603 603 border-left: 1px solid #ccc;
604 604 border-right: 1px solid #ccc;
605 605 }
606 606 div.autocomplete ul li span.informal {
607 607 font-size: 80%;
608 608 color: #aaa;
609 609 }
610 610
611 611 /***** Diff *****/
612 612 .diff_out { background: #fcc; }
613 613 .diff_in { background: #cfc; }
614 614
615 615 /***** Wiki *****/
616 616 div.wiki table {
617 617 border: 1px solid #505050;
618 618 border-collapse: collapse;
619 619 margin-bottom: 1em;
620 620 }
621 621
622 622 div.wiki table, div.wiki td, div.wiki th {
623 623 border: 1px solid #bbb;
624 624 padding: 4px;
625 625 }
626 626
627 627 div.wiki .external {
628 628 background-position: 0% 60%;
629 629 background-repeat: no-repeat;
630 630 padding-left: 12px;
631 631 background-image: url(../images/external.png);
632 632 }
633 633
634 634 div.wiki a.new {
635 635 color: #b73535;
636 636 }
637 637
638 638 div.wiki pre {
639 639 margin: 1em 1em 1em 1.6em;
640 640 padding: 2px;
641 641 background-color: #fafafa;
642 642 border: 1px solid #dadada;
643 643 width:95%;
644 644 overflow-x: auto;
645 645 }
646 646
647 647 div.wiki ul.toc {
648 648 background-color: #ffffdd;
649 649 border: 1px solid #e4e4e4;
650 650 padding: 4px;
651 651 line-height: 1.2em;
652 652 margin-bottom: 12px;
653 653 margin-right: 12px;
654 654 margin-left: 0;
655 655 display: table
656 656 }
657 657 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
658 658
659 659 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
660 660 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
661 661 div.wiki ul.toc li { list-style-type:none;}
662 662 div.wiki ul.toc li.heading2 { margin-left: 6px; }
663 663 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
664 664
665 665 div.wiki ul.toc a {
666 666 font-size: 0.9em;
667 667 font-weight: normal;
668 668 text-decoration: none;
669 669 color: #606060;
670 670 }
671 671 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
672 672
673 673 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
674 674 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
675 675 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
676 676
677 677 div.wiki img { vertical-align: middle; }
678 678
679 679 /***** My page layout *****/
680 680 .block-receiver {
681 681 border:1px dashed #c0c0c0;
682 682 margin-bottom: 20px;
683 683 padding: 15px 0 15px 0;
684 684 }
685 685
686 686 .mypage-box {
687 687 margin:0 0 20px 0;
688 688 color:#505050;
689 689 line-height:1.5em;
690 690 }
691 691
692 692 .handle {
693 693 cursor: move;
694 694 }
695 695
696 696 a.close-icon {
697 697 display:block;
698 698 margin-top:3px;
699 699 overflow:hidden;
700 700 width:12px;
701 701 height:12px;
702 702 background-repeat: no-repeat;
703 703 cursor:pointer;
704 704 background-image:url('../images/close.png');
705 705 }
706 706
707 707 a.close-icon:hover {
708 708 background-image:url('../images/close_hl.png');
709 709 }
710 710
711 711 /***** Gantt chart *****/
712 712 .gantt_hdr {
713 713 position:absolute;
714 714 top:0;
715 715 height:16px;
716 716 border-top: 1px solid #c0c0c0;
717 717 border-bottom: 1px solid #c0c0c0;
718 718 border-right: 1px solid #c0c0c0;
719 719 text-align: center;
720 720 overflow: hidden;
721 721 }
722 722
723 723 .task {
724 724 position: absolute;
725 725 height:8px;
726 726 font-size:0.8em;
727 727 color:#888;
728 728 padding:0;
729 729 margin:0;
730 730 line-height:0.8em;
731 white-space:nowrap;
731 732 }
732 733
733 734 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
734 735 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
735 736 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
736 737 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
737 738
738 739 /***** Icons *****/
739 740 .icon {
740 741 background-position: 0% 50%;
741 742 background-repeat: no-repeat;
742 743 padding-left: 20px;
743 744 padding-top: 2px;
744 745 padding-bottom: 3px;
745 746 }
746 747
747 748 .icon-add { background-image: url(../images/add.png); }
748 749 .icon-edit { background-image: url(../images/edit.png); }
749 750 .icon-copy { background-image: url(../images/copy.png); }
750 751 .icon-duplicate { background-image: url(../images/duplicate.png); }
751 752 .icon-del { background-image: url(../images/delete.png); }
752 753 .icon-move { background-image: url(../images/move.png); }
753 754 .icon-save { background-image: url(../images/save.png); }
754 755 .icon-cancel { background-image: url(../images/cancel.png); }
755 756 .icon-multiple { background-image: url(../images/table_multiple.png); }
756 757 .icon-folder { background-image: url(../images/folder.png); }
757 758 .open .icon-folder { background-image: url(../images/folder_open.png); }
758 759 .icon-package { background-image: url(../images/package.png); }
759 760 .icon-home { background-image: url(../images/home.png); }
760 761 .icon-user { background-image: url(../images/user.png); }
761 762 .icon-projects { background-image: url(../images/projects.png); }
762 763 .icon-help { background-image: url(../images/help.png); }
763 764 .icon-attachment { background-image: url(../images/attachment.png); }
764 765 .icon-history { background-image: url(../images/history.png); }
765 766 .icon-time { background-image: url(../images/time.png); }
766 767 .icon-time-add { background-image: url(../images/time_add.png); }
767 768 .icon-stats { background-image: url(../images/stats.png); }
768 769 .icon-warning { background-image: url(../images/warning.png); }
769 770 .icon-fav { background-image: url(../images/fav.png); }
770 771 .icon-fav-off { background-image: url(../images/fav_off.png); }
771 772 .icon-reload { background-image: url(../images/reload.png); }
772 773 .icon-lock { background-image: url(../images/locked.png); }
773 774 .icon-unlock { background-image: url(../images/unlock.png); }
774 775 .icon-checked { background-image: url(../images/true.png); }
775 776 .icon-details { background-image: url(../images/zoom_in.png); }
776 777 .icon-report { background-image: url(../images/report.png); }
777 778 .icon-comment { background-image: url(../images/comment.png); }
778 779 .icon-summary { background-image: url(../images/lightning.png); }
779 780
780 781 .icon-file { background-image: url(../images/files/default.png); }
781 782 .icon-file.text-plain { background-image: url(../images/files/text.png); }
782 783 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
783 784 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
784 785 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
785 786 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
786 787 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
787 788 .icon-file.image-gif { background-image: url(../images/files/image.png); }
788 789 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
789 790 .icon-file.image-png { background-image: url(../images/files/image.png); }
790 791 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
791 792 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
792 793 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
793 794 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
794 795
795 796 img.gravatar {
796 797 padding: 2px;
797 798 border: solid 1px #d5d5d5;
798 799 background: #fff;
799 800 }
800 801
801 802 div.issue img.gravatar {
802 803 float: right;
803 804 margin: 0 0 0 1em;
804 805 padding: 5px;
805 806 }
806 807
807 808 div.issue table img.gravatar {
808 809 height: 14px;
809 810 width: 14px;
810 811 padding: 2px;
811 812 float: left;
812 813 margin: 0 0.5em 0 0;
813 814 }
814 815
815 816 h2 img.gravatar {
816 817 padding: 3px;
817 818 margin: -2px 4px 0 0;
818 819 float: left;
819 820 }
820 821
821 822 h4 img.gravatar {
822 823 padding: 3px;
823 824 margin: -6px 4px 0 0;
824 825 float: left;
825 826 }
826 827
827 828 td.username img.gravatar {
828 829 float: left;
829 830 margin: 0 1em 0 0;
830 831 }
831 832
832 833 #activity dt img.gravatar {
833 834 float: left;
834 835 margin: 0 1em 1em 0;
835 836 }
836 837
837 838 #activity dt,
838 839 .journal {
839 840 clear: left;
840 841 }
841 842
842 843 h2 img { vertical-align:middle; }
843 844
844 845 .hascontextmenu { cursor: context-menu; }
845 846
846 847 /***** Media print specific styles *****/
847 848 @media print {
848 849 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
849 850 #main { background: #fff; }
850 851 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
851 852 #wiki_add_attachment { display:none; }
852 853 }
@@ -1,74 +1,98
1 1 ---
2 2 wiki_contents_001:
3 3 text: |-
4 4 h1. CookBook documentation
5 5
6 6 {{child_pages}}
7 7
8 8 Some updated [[documentation]] here with gzipped history
9 9 updated_on: 2007-03-07 00:10:51 +01:00
10 10 page_id: 1
11 11 id: 1
12 12 version: 3
13 13 author_id: 1
14 14 comments: Gzip compression activated
15 15 wiki_contents_002:
16 16 text: |-
17 17 h1. Another page
18 18
19 19 This is a link to a ticket: #2
20 20 And this is an included page:
21 21 {{include(Page with an inline image)}}
22 22 updated_on: 2007-03-08 00:18:07 +01:00
23 23 page_id: 2
24 24 id: 2
25 25 version: 1
26 26 author_id: 1
27 27 comments:
28 28 wiki_contents_003:
29 29 text: |-
30 30 h1. Start page
31 31
32 32 E-commerce web site start page
33 33 updated_on: 2007-03-08 00:18:07 +01:00
34 34 page_id: 3
35 35 id: 3
36 36 version: 1
37 37 author_id: 1
38 38 comments:
39 39 wiki_contents_004:
40 40 text: |-
41 41 h1. Page with an inline image
42 42
43 43 This is an inline image:
44 44
45 45 !logo.gif!
46 46 updated_on: 2007-03-08 00:18:07 +01:00
47 47 page_id: 4
48 48 id: 4
49 49 version: 1
50 50 author_id: 1
51 51 comments:
52 52 wiki_contents_005:
53 53 text: |-
54 54 h1. Child page 1
55 55
56 56 This is a child page
57 57 updated_on: 2007-03-08 00:18:07 +01:00
58 58 page_id: 5
59 59 id: 5
60 60 version: 1
61 61 author_id: 1
62 62 comments:
63 63 wiki_contents_006:
64 64 text: |-
65 65 h1. Child page 2
66 66
67 67 This is a child page
68 68 updated_on: 2007-03-08 00:18:07 +01:00
69 69 page_id: 6
70 70 id: 6
71 71 version: 1
72 72 author_id: 1
73 73 comments:
74 wiki_contents_007:
75 text: This is a child page
76 updated_on: 2007-03-08 00:18:07 +01:00
77 page_id: 7
78 id: 7
79 version: 1
80 author_id: 1
81 comments:
82 wiki_contents_008:
83 text: This is a parent page
84 updated_on: 2007-03-08 00:18:07 +01:00
85 page_id: 8
86 id: 8
87 version: 1
88 author_id: 1
89 comments:
90 wiki_contents_009:
91 text: This is a child page
92 updated_on: 2007-03-08 00:18:07 +01:00
93 page_id: 9
94 id: 9
95 version: 1
96 author_id: 1
97 comments:
74 98 No newline at end of file
@@ -1,44 +1,65
1 1 ---
2 2 wiki_pages_001:
3 3 created_on: 2007-03-07 00:08:07 +01:00
4 4 title: CookBook_documentation
5 5 id: 1
6 6 wiki_id: 1
7 7 protected: true
8 8 parent_id:
9 9 wiki_pages_002:
10 10 created_on: 2007-03-08 00:18:07 +01:00
11 11 title: Another_page
12 12 id: 2
13 13 wiki_id: 1
14 14 protected: false
15 15 parent_id:
16 16 wiki_pages_003:
17 17 created_on: 2007-03-08 00:18:07 +01:00
18 18 title: Start_page
19 19 id: 3
20 20 wiki_id: 2
21 21 protected: false
22 22 parent_id:
23 23 wiki_pages_004:
24 24 created_on: 2007-03-08 00:18:07 +01:00
25 25 title: Page_with_an_inline_image
26 26 id: 4
27 27 wiki_id: 1
28 28 protected: false
29 29 parent_id: 1
30 30 wiki_pages_005:
31 31 created_on: 2007-03-08 00:18:07 +01:00
32 32 title: Child_1
33 33 id: 5
34 34 wiki_id: 1
35 35 protected: false
36 36 parent_id: 2
37 37 wiki_pages_006:
38 38 created_on: 2007-03-08 00:18:07 +01:00
39 39 title: Child_2
40 40 id: 6
41 41 wiki_id: 1
42 42 protected: false
43 43 parent_id: 2
44 wiki_pages_007:
45 created_on: 2007-03-08 00:18:07 +01:00
46 title: Child_page_1
47 id: 7
48 wiki_id: 2
49 protected: false
50 parent_id: 8
51 wiki_pages_008:
52 created_on: 2007-03-08 00:18:07 +01:00
53 title: Parent_page
54 id: 8
55 wiki_id: 2
56 protected: false
57 parent_id:
58 wiki_pages_009:
59 created_on: 2007-03-08 00:18:07 +01:00
60 title: Child_page_2
61 id: 9
62 wiki_id: 2
63 protected: false
64 parent_id: 8
44 65 No newline at end of file
@@ -1,67 +1,85
1 1 require File.dirname(__FILE__) + '/../test_helper'
2 2 require 'issue_relations_controller'
3 3
4 4 # Re-raise errors caught by the controller.
5 5 class IssueRelationsController; def rescue_action(e) raise e end; end
6 6
7 7
8 8 class IssueRelationsControllerTest < ActionController::TestCase
9 9 fixtures :projects,
10 10 :users,
11 11 :roles,
12 12 :members,
13 13 :member_roles,
14 14 :issues,
15 15 :issue_statuses,
16 16 :issue_relations,
17 17 :enabled_modules,
18 18 :enumerations,
19 19 :trackers
20 20
21 21 def setup
22 22 @controller = IssueRelationsController.new
23 23 @request = ActionController::TestRequest.new
24 24 @response = ActionController::TestResponse.new
25 25 User.current = nil
26 26 end
27 27
28 28 def test_new_routing
29 29 assert_routing(
30 30 {:method => :post, :path => '/issues/1/relations'},
31 31 {:controller => 'issue_relations', :action => 'new', :issue_id => '1'}
32 32 )
33 33 end
34 34
35 35 def test_new
36 36 assert_difference 'IssueRelation.count' do
37 37 @request.session[:user_id] = 3
38 38 post :new, :issue_id => 1,
39 39 :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''}
40 40 end
41 41 end
42 42
43 def test_new_should_accept_id_with_hash
44 assert_difference 'IssueRelation.count' do
45 @request.session[:user_id] = 3
46 post :new, :issue_id => 1,
47 :relation => {:issue_to_id => '#2', :relation_type => 'relates', :delay => ''}
48 end
49 end
50
51 def test_new_should_not_break_with_non_numerical_id
52 assert_no_difference 'IssueRelation.count' do
53 assert_nothing_raised do
54 @request.session[:user_id] = 3
55 post :new, :issue_id => 1,
56 :relation => {:issue_to_id => 'foo', :relation_type => 'relates', :delay => ''}
57 end
58 end
59 end
60
43 61 def test_should_create_relations_with_visible_issues_only
44 62 Setting.cross_project_issue_relations = '1'
45 63 assert_nil Issue.visible(User.find(3)).find_by_id(4)
46 64
47 65 assert_no_difference 'IssueRelation.count' do
48 66 @request.session[:user_id] = 3
49 67 post :new, :issue_id => 1,
50 68 :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''}
51 69 end
52 70 end
53 71
54 72 def test_destroy_routing
55 73 assert_recognizes( #TODO: use DELETE on issue URI
56 74 {:controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23'},
57 75 {:method => :post, :path => '/issues/1/relations/23/destroy'}
58 76 )
59 77 end
60 78
61 79 def test_destroy
62 80 assert_difference 'IssueRelation.count', -1 do
63 81 @request.session[:user_id] = 3
64 82 post :destroy, :id => '2', :issue_id => '3'
65 83 end
66 84 end
67 85 end
@@ -1,783 +1,791
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 :name
33 33 should_validate_uniqueness_of :identifier
34 34
35 35 context "associations" do
36 36 should_have_many :members
37 37 should_have_many :users, :through => :members
38 38 should_have_many :member_principals
39 39 should_have_many :principals, :through => :member_principals
40 40 should_have_many :enabled_modules
41 41 should_have_many :issues
42 42 should_have_many :issue_changes, :through => :issues
43 43 should_have_many :versions
44 44 should_have_many :time_entries
45 45 should_have_many :queries
46 46 should_have_many :documents
47 47 should_have_many :news
48 48 should_have_many :issue_categories
49 49 should_have_many :boards
50 50 should_have_many :changesets, :through => :repository
51 51
52 52 should_have_one :repository
53 53 should_have_one :wiki
54 54
55 55 should_have_and_belong_to_many :trackers
56 56 should_have_and_belong_to_many :issue_custom_fields
57 57 end
58 58
59 59 def test_truth
60 60 assert_kind_of Project, @ecookbook
61 61 assert_equal "eCookbook", @ecookbook.name
62 62 end
63 63
64 64 def test_update
65 65 assert_equal "eCookbook", @ecookbook.name
66 66 @ecookbook.name = "eCook"
67 67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 68 @ecookbook.reload
69 69 assert_equal "eCook", @ecookbook.name
70 70 end
71 71
72 72 def test_validate_identifier
73 73 to_test = {"abc" => true,
74 74 "ab12" => true,
75 75 "ab-12" => true,
76 76 "12" => false,
77 77 "new" => false}
78 78
79 79 to_test.each do |identifier, valid|
80 80 p = Project.new
81 81 p.identifier = identifier
82 82 p.valid?
83 83 assert_equal valid, p.errors.on('identifier').nil?
84 84 end
85 85 end
86 86
87 87 def test_members_should_be_active_users
88 88 Project.all.each do |project|
89 89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 90 end
91 91 end
92 92
93 93 def test_users_should_be_active_users
94 94 Project.all.each do |project|
95 95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 96 end
97 97 end
98 98
99 99 def test_archive
100 100 user = @ecookbook.members.first.user
101 101 @ecookbook.archive
102 102 @ecookbook.reload
103 103
104 104 assert !@ecookbook.active?
105 105 assert !user.projects.include?(@ecookbook)
106 106 # Subproject are also archived
107 107 assert !@ecookbook.children.empty?
108 108 assert @ecookbook.descendants.active.empty?
109 109 end
110 110
111 111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 112 # Assign an issue of a project to a version of a child project
113 113 Issue.find(4).update_attribute :fixed_version_id, 4
114 114
115 115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 116 assert_equal false, @ecookbook.archive
117 117 end
118 118 @ecookbook.reload
119 119 assert @ecookbook.active?
120 120 end
121 121
122 122 def test_unarchive
123 123 user = @ecookbook.members.first.user
124 124 @ecookbook.archive
125 125 # A subproject of an archived project can not be unarchived
126 126 assert !@ecookbook_sub1.unarchive
127 127
128 128 # Unarchive project
129 129 assert @ecookbook.unarchive
130 130 @ecookbook.reload
131 131 assert @ecookbook.active?
132 132 assert user.projects.include?(@ecookbook)
133 133 # Subproject can now be unarchived
134 134 @ecookbook_sub1.reload
135 135 assert @ecookbook_sub1.unarchive
136 136 end
137 137
138 138 def test_destroy
139 139 # 2 active members
140 140 assert_equal 2, @ecookbook.members.size
141 141 # and 1 is locked
142 142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
143 143 # some boards
144 144 assert @ecookbook.boards.any?
145 145
146 146 @ecookbook.destroy
147 147 # make sure that the project non longer exists
148 148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
149 149 # make sure related data was removed
150 150 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
151 151 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
152 152 end
153 153
154 154 def test_move_an_orphan_project_to_a_root_project
155 155 sub = Project.find(2)
156 156 sub.set_parent! @ecookbook
157 157 assert_equal @ecookbook.id, sub.parent.id
158 158 @ecookbook.reload
159 159 assert_equal 4, @ecookbook.children.size
160 160 end
161 161
162 162 def test_move_an_orphan_project_to_a_subproject
163 163 sub = Project.find(2)
164 164 assert sub.set_parent!(@ecookbook_sub1)
165 165 end
166 166
167 167 def test_move_a_root_project_to_a_project
168 168 sub = @ecookbook
169 169 assert sub.set_parent!(Project.find(2))
170 170 end
171 171
172 172 def test_should_not_move_a_project_to_its_children
173 173 sub = @ecookbook
174 174 assert !(sub.set_parent!(Project.find(3)))
175 175 end
176 176
177 177 def test_set_parent_should_add_roots_in_alphabetical_order
178 178 ProjectCustomField.delete_all
179 179 Project.delete_all
180 180 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
181 181 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
182 182 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
183 183 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
184 184
185 185 assert_equal 4, Project.count
186 186 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
187 187 end
188 188
189 189 def test_set_parent_should_add_children_in_alphabetical_order
190 190 ProjectCustomField.delete_all
191 191 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
192 192 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
193 193 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
194 194 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
195 195 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
196 196
197 197 parent.reload
198 198 assert_equal 4, parent.children.size
199 199 assert_equal parent.children.sort_by(&:name), parent.children
200 200 end
201 201
202 202 def test_rebuild_should_sort_children_alphabetically
203 203 ProjectCustomField.delete_all
204 204 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
205 205 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
206 206 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
207 207 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
208 208 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
209 209
210 210 Project.update_all("lft = NULL, rgt = NULL")
211 211 Project.rebuild!
212 212
213 213 parent.reload
214 214 assert_equal 4, parent.children.size
215 215 assert_equal parent.children.sort_by(&:name), parent.children
216 216 end
217 217
218 218
219 219 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
220 220 # Parent issue with a hierarchy project's fixed version
221 221 parent_issue = Issue.find(1)
222 222 parent_issue.update_attribute(:fixed_version_id, 4)
223 223 parent_issue.reload
224 224 assert_equal 4, parent_issue.fixed_version_id
225 225
226 226 # Should keep fixed versions for the issues
227 227 issue_with_local_fixed_version = Issue.find(5)
228 228 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
229 229 issue_with_local_fixed_version.reload
230 230 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
231 231
232 232 # Local issue with hierarchy fixed_version
233 233 issue_with_hierarchy_fixed_version = Issue.find(13)
234 234 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
235 235 issue_with_hierarchy_fixed_version.reload
236 236 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
237 237
238 238 # Move project out of the issue's hierarchy
239 239 moved_project = Project.find(3)
240 240 moved_project.set_parent!(Project.find(2))
241 241 parent_issue.reload
242 242 issue_with_local_fixed_version.reload
243 243 issue_with_hierarchy_fixed_version.reload
244 244
245 245 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
246 246 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"
247 247 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
248 248 end
249 249
250 250 def test_parent
251 251 p = Project.find(6).parent
252 252 assert p.is_a?(Project)
253 253 assert_equal 5, p.id
254 254 end
255 255
256 256 def test_ancestors
257 257 a = Project.find(6).ancestors
258 258 assert a.first.is_a?(Project)
259 259 assert_equal [1, 5], a.collect(&:id)
260 260 end
261 261
262 262 def test_root
263 263 r = Project.find(6).root
264 264 assert r.is_a?(Project)
265 265 assert_equal 1, r.id
266 266 end
267 267
268 268 def test_children
269 269 c = Project.find(1).children
270 270 assert c.first.is_a?(Project)
271 271 assert_equal [5, 3, 4], c.collect(&:id)
272 272 end
273 273
274 274 def test_descendants
275 275 d = Project.find(1).descendants
276 276 assert d.first.is_a?(Project)
277 277 assert_equal [5, 6, 3, 4], d.collect(&:id)
278 278 end
279 279
280 280 def test_allowed_parents_should_be_empty_for_non_member_user
281 281 Role.non_member.add_permission!(:add_project)
282 282 user = User.find(9)
283 283 assert user.memberships.empty?
284 284 User.current = user
285 285 assert Project.new.allowed_parents.compact.empty?
286 286 end
287 287
288 288 def test_allowed_parents_with_add_subprojects_permission
289 289 Role.find(1).remove_permission!(:add_project)
290 290 Role.find(1).add_permission!(:add_subprojects)
291 291 User.current = User.find(2)
292 292 # new project
293 293 assert !Project.new.allowed_parents.include?(nil)
294 294 assert Project.new.allowed_parents.include?(Project.find(1))
295 295 # existing root project
296 296 assert Project.find(1).allowed_parents.include?(nil)
297 297 # existing child
298 298 assert Project.find(3).allowed_parents.include?(Project.find(1))
299 299 assert !Project.find(3).allowed_parents.include?(nil)
300 300 end
301 301
302 302 def test_allowed_parents_with_add_project_permission
303 303 Role.find(1).add_permission!(:add_project)
304 304 Role.find(1).remove_permission!(:add_subprojects)
305 305 User.current = User.find(2)
306 306 # new project
307 307 assert Project.new.allowed_parents.include?(nil)
308 308 assert !Project.new.allowed_parents.include?(Project.find(1))
309 309 # existing root project
310 310 assert Project.find(1).allowed_parents.include?(nil)
311 311 # existing child
312 312 assert Project.find(3).allowed_parents.include?(Project.find(1))
313 313 assert Project.find(3).allowed_parents.include?(nil)
314 314 end
315 315
316 316 def test_allowed_parents_with_add_project_and_subprojects_permission
317 317 Role.find(1).add_permission!(:add_project)
318 318 Role.find(1).add_permission!(:add_subprojects)
319 319 User.current = User.find(2)
320 320 # new project
321 321 assert Project.new.allowed_parents.include?(nil)
322 322 assert Project.new.allowed_parents.include?(Project.find(1))
323 323 # existing root project
324 324 assert Project.find(1).allowed_parents.include?(nil)
325 325 # existing child
326 326 assert Project.find(3).allowed_parents.include?(Project.find(1))
327 327 assert Project.find(3).allowed_parents.include?(nil)
328 328 end
329 329
330 330 def test_users_by_role
331 331 users_by_role = Project.find(1).users_by_role
332 332 assert_kind_of Hash, users_by_role
333 333 role = Role.find(1)
334 334 assert_kind_of Array, users_by_role[role]
335 335 assert users_by_role[role].include?(User.find(2))
336 336 end
337 337
338 338 def test_rolled_up_trackers
339 339 parent = Project.find(1)
340 340 parent.trackers = Tracker.find([1,2])
341 341 child = parent.children.find(3)
342 342
343 343 assert_equal [1, 2], parent.tracker_ids
344 344 assert_equal [2, 3], child.trackers.collect(&:id)
345 345
346 346 assert_kind_of Tracker, parent.rolled_up_trackers.first
347 347 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
348 348
349 349 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
350 350 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
351 351 end
352 352
353 353 def test_rolled_up_trackers_should_ignore_archived_subprojects
354 354 parent = Project.find(1)
355 355 parent.trackers = Tracker.find([1,2])
356 356 child = parent.children.find(3)
357 357 child.trackers = Tracker.find([1,3])
358 358 parent.children.each(&:archive)
359 359
360 360 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
361 361 end
362 362
363 363 def test_shared_versions_none_sharing
364 364 p = Project.find(5)
365 365 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
366 366 assert p.shared_versions.include?(v)
367 367 assert !p.children.first.shared_versions.include?(v)
368 368 assert !p.root.shared_versions.include?(v)
369 369 assert !p.siblings.first.shared_versions.include?(v)
370 370 assert !p.root.siblings.first.shared_versions.include?(v)
371 371 end
372 372
373 373 def test_shared_versions_descendants_sharing
374 374 p = Project.find(5)
375 375 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
376 376 assert p.shared_versions.include?(v)
377 377 assert p.children.first.shared_versions.include?(v)
378 378 assert !p.root.shared_versions.include?(v)
379 379 assert !p.siblings.first.shared_versions.include?(v)
380 380 assert !p.root.siblings.first.shared_versions.include?(v)
381 381 end
382 382
383 383 def test_shared_versions_hierarchy_sharing
384 384 p = Project.find(5)
385 385 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
386 386 assert p.shared_versions.include?(v)
387 387 assert p.children.first.shared_versions.include?(v)
388 388 assert p.root.shared_versions.include?(v)
389 389 assert !p.siblings.first.shared_versions.include?(v)
390 390 assert !p.root.siblings.first.shared_versions.include?(v)
391 391 end
392 392
393 393 def test_shared_versions_tree_sharing
394 394 p = Project.find(5)
395 395 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
396 396 assert p.shared_versions.include?(v)
397 397 assert p.children.first.shared_versions.include?(v)
398 398 assert p.root.shared_versions.include?(v)
399 399 assert p.siblings.first.shared_versions.include?(v)
400 400 assert !p.root.siblings.first.shared_versions.include?(v)
401 401 end
402 402
403 403 def test_shared_versions_system_sharing
404 404 p = Project.find(5)
405 405 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
406 406 assert p.shared_versions.include?(v)
407 407 assert p.children.first.shared_versions.include?(v)
408 408 assert p.root.shared_versions.include?(v)
409 409 assert p.siblings.first.shared_versions.include?(v)
410 410 assert p.root.siblings.first.shared_versions.include?(v)
411 411 end
412 412
413 413 def test_shared_versions
414 414 parent = Project.find(1)
415 415 child = parent.children.find(3)
416 416 private_child = parent.children.find(5)
417 417
418 418 assert_equal [1,2,3], parent.version_ids.sort
419 419 assert_equal [4], child.version_ids
420 420 assert_equal [6], private_child.version_ids
421 421 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
422 422
423 423 assert_equal 6, parent.shared_versions.size
424 424 parent.shared_versions.each do |version|
425 425 assert_kind_of Version, version
426 426 end
427 427
428 428 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
429 429 end
430 430
431 431 def test_shared_versions_should_ignore_archived_subprojects
432 432 parent = Project.find(1)
433 433 child = parent.children.find(3)
434 434 child.archive
435 435 parent.reload
436 436
437 437 assert_equal [1,2,3], parent.version_ids.sort
438 438 assert_equal [4], child.version_ids
439 439 assert !parent.shared_versions.collect(&:id).include?(4)
440 440 end
441 441
442 442 def test_shared_versions_visible_to_user
443 443 user = User.find(3)
444 444 parent = Project.find(1)
445 445 child = parent.children.find(5)
446 446
447 447 assert_equal [1,2,3], parent.version_ids.sort
448 448 assert_equal [6], child.version_ids
449 449
450 450 versions = parent.shared_versions.visible(user)
451 451
452 452 assert_equal 4, versions.size
453 453 versions.each do |version|
454 454 assert_kind_of Version, version
455 455 end
456 456
457 457 assert !versions.collect(&:id).include?(6)
458 458 end
459 459
460 460
461 461 def test_next_identifier
462 462 ProjectCustomField.delete_all
463 463 Project.create!(:name => 'last', :identifier => 'p2008040')
464 464 assert_equal 'p2008041', Project.next_identifier
465 465 end
466 466
467 467 def test_next_identifier_first_project
468 468 Project.delete_all
469 469 assert_nil Project.next_identifier
470 470 end
471 471
472 472
473 473 def test_enabled_module_names_should_not_recreate_enabled_modules
474 474 project = Project.find(1)
475 475 # Remove one module
476 476 modules = project.enabled_modules.slice(0..-2)
477 477 assert modules.any?
478 478 assert_difference 'EnabledModule.count', -1 do
479 479 project.enabled_module_names = modules.collect(&:name)
480 480 end
481 481 project.reload
482 482 # Ids should be preserved
483 483 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
484 484 end
485 485
486 486 def test_copy_from_existing_project
487 487 source_project = Project.find(1)
488 488 copied_project = Project.copy_from(1)
489 489
490 490 assert copied_project
491 491 # Cleared attributes
492 492 assert copied_project.id.blank?
493 493 assert copied_project.name.blank?
494 494 assert copied_project.identifier.blank?
495 495
496 496 # Duplicated attributes
497 497 assert_equal source_project.description, copied_project.description
498 498 assert_equal source_project.enabled_modules, copied_project.enabled_modules
499 499 assert_equal source_project.trackers, copied_project.trackers
500 500
501 501 # Default attributes
502 502 assert_equal 1, copied_project.status
503 503 end
504 504
505 505 def test_activities_should_use_the_system_activities
506 506 project = Project.find(1)
507 507 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
508 508 end
509 509
510 510
511 511 def test_activities_should_use_the_project_specific_activities
512 512 project = Project.find(1)
513 513 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
514 514 assert overridden_activity.save!
515 515
516 516 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
517 517 end
518 518
519 519 def test_activities_should_not_include_the_inactive_project_specific_activities
520 520 project = Project.find(1)
521 521 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
522 522 assert overridden_activity.save!
523 523
524 524 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
525 525 end
526 526
527 527 def test_activities_should_not_include_project_specific_activities_from_other_projects
528 528 project = Project.find(1)
529 529 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
530 530 assert overridden_activity.save!
531 531
532 532 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
533 533 end
534 534
535 535 def test_activities_should_handle_nils
536 536 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
537 537 TimeEntryActivity.delete_all
538 538
539 539 # No activities
540 540 project = Project.find(1)
541 541 assert project.activities.empty?
542 542
543 543 # No system, one overridden
544 544 assert overridden_activity.save!
545 545 project.reload
546 546 assert_equal [overridden_activity], project.activities
547 547 end
548 548
549 549 def test_activities_should_override_system_activities_with_project_activities
550 550 project = Project.find(1)
551 551 parent_activity = TimeEntryActivity.find(:first)
552 552 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
553 553 assert overridden_activity.save!
554 554
555 555 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
556 556 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
557 557 end
558 558
559 559 def test_activities_should_include_inactive_activities_if_specified
560 560 project = Project.find(1)
561 561 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
562 562 assert overridden_activity.save!
563 563
564 564 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
565 565 end
566 566
567 567 test 'activities should not include active System activities if the project has an override that is inactive' do
568 568 project = Project.find(1)
569 569 system_activity = TimeEntryActivity.find_by_name('Design')
570 570 assert system_activity.active?
571 571 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
572 572 assert overridden_activity.save!
573 573
574 574 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
575 575 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
576 576 end
577 577
578 578 def test_close_completed_versions
579 579 Version.update_all("status = 'open'")
580 580 project = Project.find(1)
581 581 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
582 582 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
583 583 project.close_completed_versions
584 584 project.reload
585 585 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
586 586 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
587 587 end
588 588
589 589 context "Project#copy" do
590 590 setup do
591 591 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
592 592 Project.destroy_all :identifier => "copy-test"
593 593 @source_project = Project.find(2)
594 594 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
595 595 @project.trackers = @source_project.trackers
596 596 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
597 597 end
598 598
599 599 should "copy issues" do
600 600 @source_project.issues << Issue.generate!(:status_id => 5,
601 601 :subject => "copy issue status",
602 602 :tracker_id => 1,
603 603 :assigned_to_id => 2,
604 604 :project_id => @source_project.id)
605 605 assert @project.valid?
606 606 assert @project.issues.empty?
607 607 assert @project.copy(@source_project)
608 608
609 609 assert_equal @source_project.issues.size, @project.issues.size
610 610 @project.issues.each do |issue|
611 611 assert issue.valid?
612 612 assert ! issue.assigned_to.blank?
613 613 assert_equal @project, issue.project
614 614 end
615 615
616 616 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
617 617 assert copied_issue
618 618 assert copied_issue.status
619 619 assert_equal "Closed", copied_issue.status.name
620 620 end
621 621
622 622 should "change the new issues to use the copied version" do
623 623 User.current = User.find(1)
624 624 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
625 625 @source_project.versions << assigned_version
626 626 assert_equal 3, @source_project.versions.size
627 627 Issue.generate_for_project!(@source_project,
628 628 :fixed_version_id => assigned_version.id,
629 629 :subject => "change the new issues to use the copied version",
630 630 :tracker_id => 1,
631 631 :project_id => @source_project.id)
632 632
633 633 assert @project.copy(@source_project)
634 634 @project.reload
635 635 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
636 636
637 637 assert copied_issue
638 638 assert copied_issue.fixed_version
639 639 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
640 640 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
641 641 end
642 642
643 643 should "copy issue relations" do
644 644 Setting.cross_project_issue_relations = '1'
645 645
646 646 second_issue = Issue.generate!(:status_id => 5,
647 647 :subject => "copy issue relation",
648 648 :tracker_id => 1,
649 649 :assigned_to_id => 2,
650 650 :project_id => @source_project.id)
651 651 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
652 652 :issue_to => second_issue,
653 653 :relation_type => "relates")
654 654 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
655 655 :issue_to => second_issue,
656 656 :relation_type => "duplicates")
657 657
658 658 assert @project.copy(@source_project)
659 659 assert_equal @source_project.issues.count, @project.issues.count
660 660 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
661 661 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
662 662
663 663 # First issue with a relation on project
664 664 assert_equal 1, copied_issue.relations.size, "Relation not copied"
665 665 copied_relation = copied_issue.relations.first
666 666 assert_equal "relates", copied_relation.relation_type
667 667 assert_equal copied_second_issue.id, copied_relation.issue_to_id
668 668 assert_not_equal source_relation.id, copied_relation.id
669 669
670 670 # Second issue with a cross project relation
671 671 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
672 672 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
673 673 assert_equal "duplicates", copied_relation.relation_type
674 674 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
675 675 assert_not_equal source_relation_cross_project.id, copied_relation.id
676 676 end
677 677
678 678 should "copy memberships" do
679 679 assert @project.valid?
680 680 assert @project.members.empty?
681 681 assert @project.copy(@source_project)
682 682
683 683 assert_equal @source_project.memberships.size, @project.memberships.size
684 684 @project.memberships.each do |membership|
685 685 assert membership
686 686 assert_equal @project, membership.project
687 687 end
688 688 end
689 689
690 690 should "copy project specific queries" do
691 691 assert @project.valid?
692 692 assert @project.queries.empty?
693 693 assert @project.copy(@source_project)
694 694
695 695 assert_equal @source_project.queries.size, @project.queries.size
696 696 @project.queries.each do |query|
697 697 assert query
698 698 assert_equal @project, query.project
699 699 end
700 700 end
701 701
702 702 should "copy versions" do
703 703 @source_project.versions << Version.generate!
704 704 @source_project.versions << Version.generate!
705 705
706 706 assert @project.versions.empty?
707 707 assert @project.copy(@source_project)
708 708
709 709 assert_equal @source_project.versions.size, @project.versions.size
710 710 @project.versions.each do |version|
711 711 assert version
712 712 assert_equal @project, version.project
713 713 end
714 714 end
715 715
716 716 should "copy wiki" do
717 717 assert_difference 'Wiki.count' do
718 718 assert @project.copy(@source_project)
719 719 end
720 720
721 721 assert @project.wiki
722 722 assert_not_equal @source_project.wiki, @project.wiki
723 723 assert_equal "Start page", @project.wiki.start_page
724 724 end
725 725
726 should "copy wiki pages and content" do
727 assert @project.copy(@source_project)
728
726 should "copy wiki pages and content with hierarchy" do
727 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
728 assert @project.copy(@source_project)
729 end
730
729 731 assert @project.wiki
730 assert_equal 1, @project.wiki.pages.length
732 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
731 733
732 734 @project.wiki.pages.each do |wiki_page|
733 735 assert wiki_page.content
734 736 assert !@source_project.wiki.pages.include?(wiki_page)
735 737 end
738
739 parent = @project.wiki.find_page('Parent_page')
740 child1 = @project.wiki.find_page('Child_page_1')
741 child2 = @project.wiki.find_page('Child_page_2')
742 assert_equal parent, child1.parent
743 assert_equal parent, child2.parent
736 744 end
737 745
738 746 should "copy issue categories" do
739 747 assert @project.copy(@source_project)
740 748
741 749 assert_equal 2, @project.issue_categories.size
742 750 @project.issue_categories.each do |issue_category|
743 751 assert !@source_project.issue_categories.include?(issue_category)
744 752 end
745 753 end
746 754
747 755 should "copy boards" do
748 756 assert @project.copy(@source_project)
749 757
750 758 assert_equal 1, @project.boards.size
751 759 @project.boards.each do |board|
752 760 assert !@source_project.boards.include?(board)
753 761 end
754 762 end
755 763
756 764 should "change the new issues to use the copied issue categories" do
757 765 issue = Issue.find(4)
758 766 issue.update_attribute(:category_id, 3)
759 767
760 768 assert @project.copy(@source_project)
761 769
762 770 @project.issues.each do |issue|
763 771 assert issue.category
764 772 assert_equal "Stock management", issue.category.name # Same name
765 773 assert_not_equal IssueCategory.find(3), issue.category # Different record
766 774 end
767 775 end
768 776
769 777 should "limit copy with :only option" do
770 778 assert @project.members.empty?
771 779 assert @project.issue_categories.empty?
772 780 assert @source_project.issues.any?
773 781
774 782 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
775 783
776 784 assert @project.members.any?
777 785 assert @project.issue_categories.any?
778 786 assert @project.issues.empty?
779 787 end
780 788
781 789 end
782 790
783 791 end
General Comments 0
You need to be logged in to leave comments. Login now