##// END OF EJS Templates
Possibility to define the default enable trackers when creating a project (#13175)....
Jean-Philippe Lang -
r11164:4e9fbeb85165
parent child
Show More
@@ -1,106 +1,106
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module SettingsHelper
20 module SettingsHelper
21 def administration_settings_tabs
21 def administration_settings_tabs
22 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
22 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
23 {:name => 'display', :partial => 'settings/display', :label => :label_display},
23 {:name => 'display', :partial => 'settings/display', :label => :label_display},
24 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
24 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
25 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
25 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
26 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
26 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
27 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
27 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
28 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
28 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
29 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
29 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
30 ]
30 ]
31 end
31 end
32
32
33 def setting_select(setting, choices, options={})
33 def setting_select(setting, choices, options={})
34 if blank_text = options.delete(:blank)
34 if blank_text = options.delete(:blank)
35 choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
35 choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
36 end
36 end
37 setting_label(setting, options).html_safe +
37 setting_label(setting, options).html_safe +
38 select_tag("settings[#{setting}]",
38 select_tag("settings[#{setting}]",
39 options_for_select(choices, Setting.send(setting).to_s),
39 options_for_select(choices, Setting.send(setting).to_s),
40 options).html_safe
40 options).html_safe
41 end
41 end
42
42
43 def setting_multiselect(setting, choices, options={})
43 def setting_multiselect(setting, choices, options={})
44 setting_values = Setting.send(setting)
44 setting_values = Setting.send(setting)
45 setting_values = [] unless setting_values.is_a?(Array)
45 setting_values = [] unless setting_values.is_a?(Array)
46
46
47 content_tag("label", l(options[:label] || "setting_#{setting}")) +
47 content_tag("label", l(options[:label] || "setting_#{setting}")) +
48 hidden_field_tag("settings[#{setting}][]", '').html_safe +
48 hidden_field_tag("settings[#{setting}][]", '').html_safe +
49 choices.collect do |choice|
49 choices.collect do |choice|
50 text, value = (choice.is_a?(Array) ? choice : [choice, choice])
50 text, value = (choice.is_a?(Array) ? choice : [choice, choice])
51 content_tag(
51 content_tag(
52 'label',
52 'label',
53 check_box_tag(
53 check_box_tag(
54 "settings[#{setting}][]",
54 "settings[#{setting}][]",
55 value,
55 value,
56 Setting.send(setting).include?(value),
56 setting_values.include?(value),
57 :id => nil
57 :id => nil
58 ) + text.to_s,
58 ) + text.to_s,
59 :class => (options[:inline] ? 'inline' : 'block')
59 :class => (options[:inline] ? 'inline' : 'block')
60 )
60 )
61 end.join.html_safe
61 end.join.html_safe
62 end
62 end
63
63
64 def setting_text_field(setting, options={})
64 def setting_text_field(setting, options={})
65 setting_label(setting, options).html_safe +
65 setting_label(setting, options).html_safe +
66 text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
66 text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
67 end
67 end
68
68
69 def setting_text_area(setting, options={})
69 def setting_text_area(setting, options={})
70 setting_label(setting, options).html_safe +
70 setting_label(setting, options).html_safe +
71 text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
71 text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
72 end
72 end
73
73
74 def setting_check_box(setting, options={})
74 def setting_check_box(setting, options={})
75 setting_label(setting, options).html_safe +
75 setting_label(setting, options).html_safe +
76 hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
76 hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
77 check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
77 check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
78 end
78 end
79
79
80 def setting_label(setting, options={})
80 def setting_label(setting, options={})
81 label = options.delete(:label)
81 label = options.delete(:label)
82 label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : ''
82 label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : ''
83 end
83 end
84
84
85 # Renders a notification field for a Redmine::Notifiable option
85 # Renders a notification field for a Redmine::Notifiable option
86 def notification_field(notifiable)
86 def notification_field(notifiable)
87 return content_tag(:label,
87 return content_tag(:label,
88 check_box_tag('settings[notified_events][]',
88 check_box_tag('settings[notified_events][]',
89 notifiable.name,
89 notifiable.name,
90 Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
90 Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
91 l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
91 l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
92 :class => notifiable.parent.present? ? "parent" : '').html_safe
92 :class => notifiable.parent.present? ? "parent" : '').html_safe
93 end
93 end
94
94
95 def cross_project_subtasks_options
95 def cross_project_subtasks_options
96 options = [
96 options = [
97 [:label_disabled, ''],
97 [:label_disabled, ''],
98 [:label_cross_project_system, 'system'],
98 [:label_cross_project_system, 'system'],
99 [:label_cross_project_tree, 'tree'],
99 [:label_cross_project_tree, 'tree'],
100 [:label_cross_project_hierarchy, 'hierarchy'],
100 [:label_cross_project_hierarchy, 'hierarchy'],
101 [:label_cross_project_descendants, 'descendants']
101 [:label_cross_project_descendants, 'descendants']
102 ]
102 ]
103
103
104 options.map {|label, value| [l(label), value.to_s]}
104 options.map {|label, value| [l(label), value.to_s]}
105 end
105 end
106 end
106 end
@@ -1,1020 +1,1025
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
86 before_destroy :delete_all_members
86 before_destroy :delete_all_members
87
87
88 scope :has_module, lambda {|mod|
88 scope :has_module, lambda {|mod|
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
90 }
90 }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 scope :all_public, lambda { where(:is_public => true) }
93 scope :all_public, lambda { where(:is_public => true) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
95 scope :allowed_to, lambda {|*args|
95 scope :allowed_to, lambda {|*args|
96 user = User.current
96 user = User.current
97 permission = nil
97 permission = nil
98 if args.first.is_a?(Symbol)
98 if args.first.is_a?(Symbol)
99 permission = args.shift
99 permission = args.shift
100 else
100 else
101 user = args.shift
101 user = args.shift
102 permission = args.shift
102 permission = args.shift
103 end
103 end
104 where(Project.allowed_to_condition(user, permission, *args))
104 where(Project.allowed_to_condition(user, permission, *args))
105 }
105 }
106 scope :like, lambda {|arg|
106 scope :like, lambda {|arg|
107 if arg.blank?
107 if arg.blank?
108 where(nil)
108 where(nil)
109 else
109 else
110 pattern = "%#{arg.to_s.strip.downcase}%"
110 pattern = "%#{arg.to_s.strip.downcase}%"
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
112 end
112 end
113 }
113 }
114
114
115 def initialize(attributes=nil, *args)
115 def initialize(attributes=nil, *args)
116 super
116 super
117
117
118 initialized = (attributes || {}).stringify_keys
118 initialized = (attributes || {}).stringify_keys
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
120 self.identifier = Project.next_identifier
120 self.identifier = Project.next_identifier
121 end
121 end
122 if !initialized.key?('is_public')
122 if !initialized.key?('is_public')
123 self.is_public = Setting.default_projects_public?
123 self.is_public = Setting.default_projects_public?
124 end
124 end
125 if !initialized.key?('enabled_module_names')
125 if !initialized.key?('enabled_module_names')
126 self.enabled_module_names = Setting.default_projects_modules
126 self.enabled_module_names = Setting.default_projects_modules
127 end
127 end
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
129 default = Setting.default_projects_tracker_ids
130 if default.is_a?(Array)
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
132 else
129 self.trackers = Tracker.sorted.all
133 self.trackers = Tracker.sorted.all
130 end
134 end
131 end
135 end
136 end
132
137
133 def identifier=(identifier)
138 def identifier=(identifier)
134 super unless identifier_frozen?
139 super unless identifier_frozen?
135 end
140 end
136
141
137 def identifier_frozen?
142 def identifier_frozen?
138 errors[:identifier].blank? && !(new_record? || identifier.blank?)
143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
139 end
144 end
140
145
141 # returns latest created projects
146 # returns latest created projects
142 # non public projects will be returned only if user is a member of those
147 # non public projects will be returned only if user is a member of those
143 def self.latest(user=nil, count=5)
148 def self.latest(user=nil, count=5)
144 visible(user).limit(count).order("created_on DESC").all
149 visible(user).limit(count).order("created_on DESC").all
145 end
150 end
146
151
147 # Returns true if the project is visible to +user+ or to the current user.
152 # Returns true if the project is visible to +user+ or to the current user.
148 def visible?(user=User.current)
153 def visible?(user=User.current)
149 user.allowed_to?(:view_project, self)
154 user.allowed_to?(:view_project, self)
150 end
155 end
151
156
152 # Returns a SQL conditions string used to find all projects visible by the specified user.
157 # Returns a SQL conditions string used to find all projects visible by the specified user.
153 #
158 #
154 # Examples:
159 # Examples:
155 # Project.visible_condition(admin) => "projects.status = 1"
160 # Project.visible_condition(admin) => "projects.status = 1"
156 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
157 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
158 def self.visible_condition(user, options={})
163 def self.visible_condition(user, options={})
159 allowed_to_condition(user, :view_project, options)
164 allowed_to_condition(user, :view_project, options)
160 end
165 end
161
166
162 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
163 #
168 #
164 # Valid options:
169 # Valid options:
165 # * :project => limit the condition to project
170 # * :project => limit the condition to project
166 # * :with_subprojects => limit the condition to project and its subprojects
171 # * :with_subprojects => limit the condition to project and its subprojects
167 # * :member => limit the condition to the user projects
172 # * :member => limit the condition to the user projects
168 def self.allowed_to_condition(user, permission, options={})
173 def self.allowed_to_condition(user, permission, options={})
169 perm = Redmine::AccessControl.permission(permission)
174 perm = Redmine::AccessControl.permission(permission)
170 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
171 if perm && perm.project_module
176 if perm && perm.project_module
172 # If the permission belongs to a project module, make sure the module is enabled
177 # If the permission belongs to a project module, make sure the module is enabled
173 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
174 end
179 end
175 if options[:project]
180 if options[:project]
176 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
177 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
178 base_statement = "(#{project_statement}) AND (#{base_statement})"
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
179 end
184 end
180
185
181 if user.admin?
186 if user.admin?
182 base_statement
187 base_statement
183 else
188 else
184 statement_by_role = {}
189 statement_by_role = {}
185 unless options[:member]
190 unless options[:member]
186 role = user.logged? ? Role.non_member : Role.anonymous
191 role = user.logged? ? Role.non_member : Role.anonymous
187 if role.allowed_to?(permission)
192 if role.allowed_to?(permission)
188 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
189 end
194 end
190 end
195 end
191 if user.logged?
196 if user.logged?
192 user.projects_by_role.each do |role, projects|
197 user.projects_by_role.each do |role, projects|
193 if role.allowed_to?(permission) && projects.any?
198 if role.allowed_to?(permission) && projects.any?
194 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
195 end
200 end
196 end
201 end
197 end
202 end
198 if statement_by_role.empty?
203 if statement_by_role.empty?
199 "1=0"
204 "1=0"
200 else
205 else
201 if block_given?
206 if block_given?
202 statement_by_role.each do |role, statement|
207 statement_by_role.each do |role, statement|
203 if s = yield(role, user)
208 if s = yield(role, user)
204 statement_by_role[role] = "(#{statement} AND (#{s}))"
209 statement_by_role[role] = "(#{statement} AND (#{s}))"
205 end
210 end
206 end
211 end
207 end
212 end
208 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
209 end
214 end
210 end
215 end
211 end
216 end
212
217
213 # Returns the Systemwide and project specific activities
218 # Returns the Systemwide and project specific activities
214 def activities(include_inactive=false)
219 def activities(include_inactive=false)
215 if include_inactive
220 if include_inactive
216 return all_activities
221 return all_activities
217 else
222 else
218 return active_activities
223 return active_activities
219 end
224 end
220 end
225 end
221
226
222 # Will create a new Project specific Activity or update an existing one
227 # Will create a new Project specific Activity or update an existing one
223 #
228 #
224 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
225 # does not successfully save.
230 # does not successfully save.
226 def update_or_create_time_entry_activity(id, activity_hash)
231 def update_or_create_time_entry_activity(id, activity_hash)
227 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
228 self.create_time_entry_activity_if_needed(activity_hash)
233 self.create_time_entry_activity_if_needed(activity_hash)
229 else
234 else
230 activity = project.time_entry_activities.find_by_id(id.to_i)
235 activity = project.time_entry_activities.find_by_id(id.to_i)
231 activity.update_attributes(activity_hash) if activity
236 activity.update_attributes(activity_hash) if activity
232 end
237 end
233 end
238 end
234
239
235 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
236 #
241 #
237 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
238 # does not successfully save.
243 # does not successfully save.
239 def create_time_entry_activity_if_needed(activity)
244 def create_time_entry_activity_if_needed(activity)
240 if activity['parent_id']
245 if activity['parent_id']
241
246
242 parent_activity = TimeEntryActivity.find(activity['parent_id'])
247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
243 activity['name'] = parent_activity.name
248 activity['name'] = parent_activity.name
244 activity['position'] = parent_activity.position
249 activity['position'] = parent_activity.position
245
250
246 if Enumeration.overridding_change?(activity, parent_activity)
251 if Enumeration.overridding_change?(activity, parent_activity)
247 project_activity = self.time_entry_activities.create(activity)
252 project_activity = self.time_entry_activities.create(activity)
248
253
249 if project_activity.new_record?
254 if project_activity.new_record?
250 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
251 else
256 else
252 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
253 end
258 end
254 end
259 end
255 end
260 end
256 end
261 end
257
262
258 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
259 #
264 #
260 # Examples:
265 # Examples:
261 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
262 # project.project_condition(false) => "projects.id = 1"
267 # project.project_condition(false) => "projects.id = 1"
263 def project_condition(with_subprojects)
268 def project_condition(with_subprojects)
264 cond = "#{Project.table_name}.id = #{id}"
269 cond = "#{Project.table_name}.id = #{id}"
265 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
266 cond
271 cond
267 end
272 end
268
273
269 def self.find(*args)
274 def self.find(*args)
270 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
271 project = find_by_identifier(*args)
276 project = find_by_identifier(*args)
272 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
273 project
278 project
274 else
279 else
275 super
280 super
276 end
281 end
277 end
282 end
278
283
279 def self.find_by_param(*args)
284 def self.find_by_param(*args)
280 self.find(*args)
285 self.find(*args)
281 end
286 end
282
287
283 def reload(*args)
288 def reload(*args)
284 @shared_versions = nil
289 @shared_versions = nil
285 @rolled_up_versions = nil
290 @rolled_up_versions = nil
286 @rolled_up_trackers = nil
291 @rolled_up_trackers = nil
287 @all_issue_custom_fields = nil
292 @all_issue_custom_fields = nil
288 @all_time_entry_custom_fields = nil
293 @all_time_entry_custom_fields = nil
289 @to_param = nil
294 @to_param = nil
290 @allowed_parents = nil
295 @allowed_parents = nil
291 @allowed_permissions = nil
296 @allowed_permissions = nil
292 @actions_allowed = nil
297 @actions_allowed = nil
293 @start_date = nil
298 @start_date = nil
294 @due_date = nil
299 @due_date = nil
295 super
300 super
296 end
301 end
297
302
298 def to_param
303 def to_param
299 # id is used for projects with a numeric identifier (compatibility)
304 # id is used for projects with a numeric identifier (compatibility)
300 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
305 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
301 end
306 end
302
307
303 def active?
308 def active?
304 self.status == STATUS_ACTIVE
309 self.status == STATUS_ACTIVE
305 end
310 end
306
311
307 def archived?
312 def archived?
308 self.status == STATUS_ARCHIVED
313 self.status == STATUS_ARCHIVED
309 end
314 end
310
315
311 # Archives the project and its descendants
316 # Archives the project and its descendants
312 def archive
317 def archive
313 # Check that there is no issue of a non descendant project that is assigned
318 # Check that there is no issue of a non descendant project that is assigned
314 # to one of the project or descendant versions
319 # to one of the project or descendant versions
315 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
320 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
316 if v_ids.any? &&
321 if v_ids.any? &&
317 Issue.
322 Issue.
318 includes(:project).
323 includes(:project).
319 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
324 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
320 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
325 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
321 exists?
326 exists?
322 return false
327 return false
323 end
328 end
324 Project.transaction do
329 Project.transaction do
325 archive!
330 archive!
326 end
331 end
327 true
332 true
328 end
333 end
329
334
330 # Unarchives the project
335 # Unarchives the project
331 # All its ancestors must be active
336 # All its ancestors must be active
332 def unarchive
337 def unarchive
333 return false if ancestors.detect {|a| !a.active?}
338 return false if ancestors.detect {|a| !a.active?}
334 update_attribute :status, STATUS_ACTIVE
339 update_attribute :status, STATUS_ACTIVE
335 end
340 end
336
341
337 def close
342 def close
338 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
343 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
339 end
344 end
340
345
341 def reopen
346 def reopen
342 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
347 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
343 end
348 end
344
349
345 # Returns an array of projects the project can be moved to
350 # Returns an array of projects the project can be moved to
346 # by the current user
351 # by the current user
347 def allowed_parents
352 def allowed_parents
348 return @allowed_parents if @allowed_parents
353 return @allowed_parents if @allowed_parents
349 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
354 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
350 @allowed_parents = @allowed_parents - self_and_descendants
355 @allowed_parents = @allowed_parents - self_and_descendants
351 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
356 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
352 @allowed_parents << nil
357 @allowed_parents << nil
353 end
358 end
354 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
359 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
355 @allowed_parents << parent
360 @allowed_parents << parent
356 end
361 end
357 @allowed_parents
362 @allowed_parents
358 end
363 end
359
364
360 # Sets the parent of the project with authorization check
365 # Sets the parent of the project with authorization check
361 def set_allowed_parent!(p)
366 def set_allowed_parent!(p)
362 unless p.nil? || p.is_a?(Project)
367 unless p.nil? || p.is_a?(Project)
363 if p.to_s.blank?
368 if p.to_s.blank?
364 p = nil
369 p = nil
365 else
370 else
366 p = Project.find_by_id(p)
371 p = Project.find_by_id(p)
367 return false unless p
372 return false unless p
368 end
373 end
369 end
374 end
370 if p.nil?
375 if p.nil?
371 if !new_record? && allowed_parents.empty?
376 if !new_record? && allowed_parents.empty?
372 return false
377 return false
373 end
378 end
374 elsif !allowed_parents.include?(p)
379 elsif !allowed_parents.include?(p)
375 return false
380 return false
376 end
381 end
377 set_parent!(p)
382 set_parent!(p)
378 end
383 end
379
384
380 # Sets the parent of the project
385 # Sets the parent of the project
381 # Argument can be either a Project, a String, a Fixnum or nil
386 # Argument can be either a Project, a String, a Fixnum or nil
382 def set_parent!(p)
387 def set_parent!(p)
383 unless p.nil? || p.is_a?(Project)
388 unless p.nil? || p.is_a?(Project)
384 if p.to_s.blank?
389 if p.to_s.blank?
385 p = nil
390 p = nil
386 else
391 else
387 p = Project.find_by_id(p)
392 p = Project.find_by_id(p)
388 return false unless p
393 return false unless p
389 end
394 end
390 end
395 end
391 if p == parent && !p.nil?
396 if p == parent && !p.nil?
392 # Nothing to do
397 # Nothing to do
393 true
398 true
394 elsif p.nil? || (p.active? && move_possible?(p))
399 elsif p.nil? || (p.active? && move_possible?(p))
395 set_or_update_position_under(p)
400 set_or_update_position_under(p)
396 Issue.update_versions_from_hierarchy_change(self)
401 Issue.update_versions_from_hierarchy_change(self)
397 true
402 true
398 else
403 else
399 # Can not move to the given target
404 # Can not move to the given target
400 false
405 false
401 end
406 end
402 end
407 end
403
408
404 # Recalculates all lft and rgt values based on project names
409 # Recalculates all lft and rgt values based on project names
405 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
410 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
406 # Used in BuildProjectsTree migration
411 # Used in BuildProjectsTree migration
407 def self.rebuild_tree!
412 def self.rebuild_tree!
408 transaction do
413 transaction do
409 update_all "lft = NULL, rgt = NULL"
414 update_all "lft = NULL, rgt = NULL"
410 rebuild!(false)
415 rebuild!(false)
411 end
416 end
412 end
417 end
413
418
414 # Returns an array of the trackers used by the project and its active sub projects
419 # Returns an array of the trackers used by the project and its active sub projects
415 def rolled_up_trackers
420 def rolled_up_trackers
416 @rolled_up_trackers ||=
421 @rolled_up_trackers ||=
417 Tracker.
422 Tracker.
418 joins(:projects).
423 joins(:projects).
419 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
424 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
420 select("DISTINCT #{Tracker.table_name}.*").
425 select("DISTINCT #{Tracker.table_name}.*").
421 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
426 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
422 sorted.
427 sorted.
423 all
428 all
424 end
429 end
425
430
426 # Closes open and locked project versions that are completed
431 # Closes open and locked project versions that are completed
427 def close_completed_versions
432 def close_completed_versions
428 Version.transaction do
433 Version.transaction do
429 versions.where(:status => %w(open locked)).all.each do |version|
434 versions.where(:status => %w(open locked)).all.each do |version|
430 if version.completed?
435 if version.completed?
431 version.update_attribute(:status, 'closed')
436 version.update_attribute(:status, 'closed')
432 end
437 end
433 end
438 end
434 end
439 end
435 end
440 end
436
441
437 # Returns a scope of the Versions on subprojects
442 # Returns a scope of the Versions on subprojects
438 def rolled_up_versions
443 def rolled_up_versions
439 @rolled_up_versions ||=
444 @rolled_up_versions ||=
440 Version.scoped(:include => :project,
445 Version.scoped(:include => :project,
441 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
446 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
442 end
447 end
443
448
444 # Returns a scope of the Versions used by the project
449 # Returns a scope of the Versions used by the project
445 def shared_versions
450 def shared_versions
446 if new_record?
451 if new_record?
447 Version.scoped(:include => :project,
452 Version.scoped(:include => :project,
448 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
453 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
449 else
454 else
450 @shared_versions ||= begin
455 @shared_versions ||= begin
451 r = root? ? self : root
456 r = root? ? self : root
452 Version.scoped(:include => :project,
457 Version.scoped(:include => :project,
453 :conditions => "#{Project.table_name}.id = #{id}" +
458 :conditions => "#{Project.table_name}.id = #{id}" +
454 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
459 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
455 " #{Version.table_name}.sharing = 'system'" +
460 " #{Version.table_name}.sharing = 'system'" +
456 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
461 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
457 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
462 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
458 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
463 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
459 "))")
464 "))")
460 end
465 end
461 end
466 end
462 end
467 end
463
468
464 # Returns a hash of project users grouped by role
469 # Returns a hash of project users grouped by role
465 def users_by_role
470 def users_by_role
466 members.includes(:user, :roles).all.inject({}) do |h, m|
471 members.includes(:user, :roles).all.inject({}) do |h, m|
467 m.roles.each do |r|
472 m.roles.each do |r|
468 h[r] ||= []
473 h[r] ||= []
469 h[r] << m.user
474 h[r] << m.user
470 end
475 end
471 h
476 h
472 end
477 end
473 end
478 end
474
479
475 # Deletes all project's members
480 # Deletes all project's members
476 def delete_all_members
481 def delete_all_members
477 me, mr = Member.table_name, MemberRole.table_name
482 me, mr = Member.table_name, MemberRole.table_name
478 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
483 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
479 Member.delete_all(['project_id = ?', id])
484 Member.delete_all(['project_id = ?', id])
480 end
485 end
481
486
482 # Users/groups issues can be assigned to
487 # Users/groups issues can be assigned to
483 def assignable_users
488 def assignable_users
484 assignable = Setting.issue_group_assignment? ? member_principals : members
489 assignable = Setting.issue_group_assignment? ? member_principals : members
485 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
490 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
486 end
491 end
487
492
488 # Returns the mail adresses of users that should be always notified on project events
493 # Returns the mail adresses of users that should be always notified on project events
489 def recipients
494 def recipients
490 notified_users.collect {|user| user.mail}
495 notified_users.collect {|user| user.mail}
491 end
496 end
492
497
493 # Returns the users that should be notified on project events
498 # Returns the users that should be notified on project events
494 def notified_users
499 def notified_users
495 # TODO: User part should be extracted to User#notify_about?
500 # TODO: User part should be extracted to User#notify_about?
496 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
501 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
497 end
502 end
498
503
499 # Returns an array of all custom fields enabled for project issues
504 # Returns an array of all custom fields enabled for project issues
500 # (explictly associated custom fields and custom fields enabled for all projects)
505 # (explictly associated custom fields and custom fields enabled for all projects)
501 def all_issue_custom_fields
506 def all_issue_custom_fields
502 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
507 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
503 end
508 end
504
509
505 # Returns an array of all custom fields enabled for project time entries
510 # Returns an array of all custom fields enabled for project time entries
506 # (explictly associated custom fields and custom fields enabled for all projects)
511 # (explictly associated custom fields and custom fields enabled for all projects)
507 def all_time_entry_custom_fields
512 def all_time_entry_custom_fields
508 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
513 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
509 end
514 end
510
515
511 def project
516 def project
512 self
517 self
513 end
518 end
514
519
515 def <=>(project)
520 def <=>(project)
516 name.downcase <=> project.name.downcase
521 name.downcase <=> project.name.downcase
517 end
522 end
518
523
519 def to_s
524 def to_s
520 name
525 name
521 end
526 end
522
527
523 # Returns a short description of the projects (first lines)
528 # Returns a short description of the projects (first lines)
524 def short_description(length = 255)
529 def short_description(length = 255)
525 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
530 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
526 end
531 end
527
532
528 def css_classes
533 def css_classes
529 s = 'project'
534 s = 'project'
530 s << ' root' if root?
535 s << ' root' if root?
531 s << ' child' if child?
536 s << ' child' if child?
532 s << (leaf? ? ' leaf' : ' parent')
537 s << (leaf? ? ' leaf' : ' parent')
533 unless active?
538 unless active?
534 if archived?
539 if archived?
535 s << ' archived'
540 s << ' archived'
536 else
541 else
537 s << ' closed'
542 s << ' closed'
538 end
543 end
539 end
544 end
540 s
545 s
541 end
546 end
542
547
543 # The earliest start date of a project, based on it's issues and versions
548 # The earliest start date of a project, based on it's issues and versions
544 def start_date
549 def start_date
545 @start_date ||= [
550 @start_date ||= [
546 issues.minimum('start_date'),
551 issues.minimum('start_date'),
547 shared_versions.minimum('effective_date'),
552 shared_versions.minimum('effective_date'),
548 Issue.fixed_version(shared_versions).minimum('start_date')
553 Issue.fixed_version(shared_versions).minimum('start_date')
549 ].compact.min
554 ].compact.min
550 end
555 end
551
556
552 # The latest due date of an issue or version
557 # The latest due date of an issue or version
553 def due_date
558 def due_date
554 @due_date ||= [
559 @due_date ||= [
555 issues.maximum('due_date'),
560 issues.maximum('due_date'),
556 shared_versions.maximum('effective_date'),
561 shared_versions.maximum('effective_date'),
557 Issue.fixed_version(shared_versions).maximum('due_date')
562 Issue.fixed_version(shared_versions).maximum('due_date')
558 ].compact.max
563 ].compact.max
559 end
564 end
560
565
561 def overdue?
566 def overdue?
562 active? && !due_date.nil? && (due_date < Date.today)
567 active? && !due_date.nil? && (due_date < Date.today)
563 end
568 end
564
569
565 # Returns the percent completed for this project, based on the
570 # Returns the percent completed for this project, based on the
566 # progress on it's versions.
571 # progress on it's versions.
567 def completed_percent(options={:include_subprojects => false})
572 def completed_percent(options={:include_subprojects => false})
568 if options.delete(:include_subprojects)
573 if options.delete(:include_subprojects)
569 total = self_and_descendants.collect(&:completed_percent).sum
574 total = self_and_descendants.collect(&:completed_percent).sum
570
575
571 total / self_and_descendants.count
576 total / self_and_descendants.count
572 else
577 else
573 if versions.count > 0
578 if versions.count > 0
574 total = versions.collect(&:completed_percent).sum
579 total = versions.collect(&:completed_percent).sum
575
580
576 total / versions.count
581 total / versions.count
577 else
582 else
578 100
583 100
579 end
584 end
580 end
585 end
581 end
586 end
582
587
583 # Return true if this project allows to do the specified action.
588 # Return true if this project allows to do the specified action.
584 # action can be:
589 # action can be:
585 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
590 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
586 # * a permission Symbol (eg. :edit_project)
591 # * a permission Symbol (eg. :edit_project)
587 def allows_to?(action)
592 def allows_to?(action)
588 if archived?
593 if archived?
589 # No action allowed on archived projects
594 # No action allowed on archived projects
590 return false
595 return false
591 end
596 end
592 unless active? || Redmine::AccessControl.read_action?(action)
597 unless active? || Redmine::AccessControl.read_action?(action)
593 # No write action allowed on closed projects
598 # No write action allowed on closed projects
594 return false
599 return false
595 end
600 end
596 # No action allowed on disabled modules
601 # No action allowed on disabled modules
597 if action.is_a? Hash
602 if action.is_a? Hash
598 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
603 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
599 else
604 else
600 allowed_permissions.include? action
605 allowed_permissions.include? action
601 end
606 end
602 end
607 end
603
608
604 def module_enabled?(module_name)
609 def module_enabled?(module_name)
605 module_name = module_name.to_s
610 module_name = module_name.to_s
606 enabled_modules.detect {|m| m.name == module_name}
611 enabled_modules.detect {|m| m.name == module_name}
607 end
612 end
608
613
609 def enabled_module_names=(module_names)
614 def enabled_module_names=(module_names)
610 if module_names && module_names.is_a?(Array)
615 if module_names && module_names.is_a?(Array)
611 module_names = module_names.collect(&:to_s).reject(&:blank?)
616 module_names = module_names.collect(&:to_s).reject(&:blank?)
612 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
617 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
613 else
618 else
614 enabled_modules.clear
619 enabled_modules.clear
615 end
620 end
616 end
621 end
617
622
618 # Returns an array of the enabled modules names
623 # Returns an array of the enabled modules names
619 def enabled_module_names
624 def enabled_module_names
620 enabled_modules.collect(&:name)
625 enabled_modules.collect(&:name)
621 end
626 end
622
627
623 # Enable a specific module
628 # Enable a specific module
624 #
629 #
625 # Examples:
630 # Examples:
626 # project.enable_module!(:issue_tracking)
631 # project.enable_module!(:issue_tracking)
627 # project.enable_module!("issue_tracking")
632 # project.enable_module!("issue_tracking")
628 def enable_module!(name)
633 def enable_module!(name)
629 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
634 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
630 end
635 end
631
636
632 # Disable a module if it exists
637 # Disable a module if it exists
633 #
638 #
634 # Examples:
639 # Examples:
635 # project.disable_module!(:issue_tracking)
640 # project.disable_module!(:issue_tracking)
636 # project.disable_module!("issue_tracking")
641 # project.disable_module!("issue_tracking")
637 # project.disable_module!(project.enabled_modules.first)
642 # project.disable_module!(project.enabled_modules.first)
638 def disable_module!(target)
643 def disable_module!(target)
639 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
644 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
640 target.destroy unless target.blank?
645 target.destroy unless target.blank?
641 end
646 end
642
647
643 safe_attributes 'name',
648 safe_attributes 'name',
644 'description',
649 'description',
645 'homepage',
650 'homepage',
646 'is_public',
651 'is_public',
647 'identifier',
652 'identifier',
648 'custom_field_values',
653 'custom_field_values',
649 'custom_fields',
654 'custom_fields',
650 'tracker_ids',
655 'tracker_ids',
651 'issue_custom_field_ids'
656 'issue_custom_field_ids'
652
657
653 safe_attributes 'enabled_module_names',
658 safe_attributes 'enabled_module_names',
654 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
659 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
655
660
656 safe_attributes 'inherit_members',
661 safe_attributes 'inherit_members',
657 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
662 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
658
663
659 # Returns an array of projects that are in this project's hierarchy
664 # Returns an array of projects that are in this project's hierarchy
660 #
665 #
661 # Example: parents, children, siblings
666 # Example: parents, children, siblings
662 def hierarchy
667 def hierarchy
663 parents = project.self_and_ancestors || []
668 parents = project.self_and_ancestors || []
664 descendants = project.descendants || []
669 descendants = project.descendants || []
665 project_hierarchy = parents | descendants # Set union
670 project_hierarchy = parents | descendants # Set union
666 end
671 end
667
672
668 # Returns an auto-generated project identifier based on the last identifier used
673 # Returns an auto-generated project identifier based on the last identifier used
669 def self.next_identifier
674 def self.next_identifier
670 p = Project.order('created_on DESC').first
675 p = Project.order('created_on DESC').first
671 p.nil? ? nil : p.identifier.to_s.succ
676 p.nil? ? nil : p.identifier.to_s.succ
672 end
677 end
673
678
674 # Copies and saves the Project instance based on the +project+.
679 # Copies and saves the Project instance based on the +project+.
675 # Duplicates the source project's:
680 # Duplicates the source project's:
676 # * Wiki
681 # * Wiki
677 # * Versions
682 # * Versions
678 # * Categories
683 # * Categories
679 # * Issues
684 # * Issues
680 # * Members
685 # * Members
681 # * Queries
686 # * Queries
682 #
687 #
683 # Accepts an +options+ argument to specify what to copy
688 # Accepts an +options+ argument to specify what to copy
684 #
689 #
685 # Examples:
690 # Examples:
686 # project.copy(1) # => copies everything
691 # project.copy(1) # => copies everything
687 # project.copy(1, :only => 'members') # => copies members only
692 # project.copy(1, :only => 'members') # => copies members only
688 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
693 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
689 def copy(project, options={})
694 def copy(project, options={})
690 project = project.is_a?(Project) ? project : Project.find(project)
695 project = project.is_a?(Project) ? project : Project.find(project)
691
696
692 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
697 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
693 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
698 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
694
699
695 Project.transaction do
700 Project.transaction do
696 if save
701 if save
697 reload
702 reload
698 to_be_copied.each do |name|
703 to_be_copied.each do |name|
699 send "copy_#{name}", project
704 send "copy_#{name}", project
700 end
705 end
701 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
706 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
702 save
707 save
703 end
708 end
704 end
709 end
705 end
710 end
706
711
707 # Returns a new unsaved Project instance with attributes copied from +project+
712 # Returns a new unsaved Project instance with attributes copied from +project+
708 def self.copy_from(project)
713 def self.copy_from(project)
709 project = project.is_a?(Project) ? project : Project.find(project)
714 project = project.is_a?(Project) ? project : Project.find(project)
710 # clear unique attributes
715 # clear unique attributes
711 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
716 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
712 copy = Project.new(attributes)
717 copy = Project.new(attributes)
713 copy.enabled_modules = project.enabled_modules
718 copy.enabled_modules = project.enabled_modules
714 copy.trackers = project.trackers
719 copy.trackers = project.trackers
715 copy.custom_values = project.custom_values.collect {|v| v.clone}
720 copy.custom_values = project.custom_values.collect {|v| v.clone}
716 copy.issue_custom_fields = project.issue_custom_fields
721 copy.issue_custom_fields = project.issue_custom_fields
717 copy
722 copy
718 end
723 end
719
724
720 # Yields the given block for each project with its level in the tree
725 # Yields the given block for each project with its level in the tree
721 def self.project_tree(projects, &block)
726 def self.project_tree(projects, &block)
722 ancestors = []
727 ancestors = []
723 projects.sort_by(&:lft).each do |project|
728 projects.sort_by(&:lft).each do |project|
724 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
729 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
725 ancestors.pop
730 ancestors.pop
726 end
731 end
727 yield project, ancestors.size
732 yield project, ancestors.size
728 ancestors << project
733 ancestors << project
729 end
734 end
730 end
735 end
731
736
732 private
737 private
733
738
734 def after_parent_changed(parent_was)
739 def after_parent_changed(parent_was)
735 remove_inherited_member_roles
740 remove_inherited_member_roles
736 add_inherited_member_roles
741 add_inherited_member_roles
737 end
742 end
738
743
739 def update_inherited_members
744 def update_inherited_members
740 if parent
745 if parent
741 if inherit_members? && !inherit_members_was
746 if inherit_members? && !inherit_members_was
742 remove_inherited_member_roles
747 remove_inherited_member_roles
743 add_inherited_member_roles
748 add_inherited_member_roles
744 elsif !inherit_members? && inherit_members_was
749 elsif !inherit_members? && inherit_members_was
745 remove_inherited_member_roles
750 remove_inherited_member_roles
746 end
751 end
747 end
752 end
748 end
753 end
749
754
750 def remove_inherited_member_roles
755 def remove_inherited_member_roles
751 member_roles = memberships.map(&:member_roles).flatten
756 member_roles = memberships.map(&:member_roles).flatten
752 member_role_ids = member_roles.map(&:id)
757 member_role_ids = member_roles.map(&:id)
753 member_roles.each do |member_role|
758 member_roles.each do |member_role|
754 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
759 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
755 member_role.destroy
760 member_role.destroy
756 end
761 end
757 end
762 end
758 end
763 end
759
764
760 def add_inherited_member_roles
765 def add_inherited_member_roles
761 if inherit_members? && parent
766 if inherit_members? && parent
762 parent.memberships.each do |parent_member|
767 parent.memberships.each do |parent_member|
763 member = Member.find_or_new(self.id, parent_member.user_id)
768 member = Member.find_or_new(self.id, parent_member.user_id)
764 parent_member.member_roles.each do |parent_member_role|
769 parent_member.member_roles.each do |parent_member_role|
765 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
770 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
766 end
771 end
767 member.save!
772 member.save!
768 end
773 end
769 end
774 end
770 end
775 end
771
776
772 # Copies wiki from +project+
777 # Copies wiki from +project+
773 def copy_wiki(project)
778 def copy_wiki(project)
774 # Check that the source project has a wiki first
779 # Check that the source project has a wiki first
775 unless project.wiki.nil?
780 unless project.wiki.nil?
776 wiki = self.wiki || Wiki.new
781 wiki = self.wiki || Wiki.new
777 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
782 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
778 wiki_pages_map = {}
783 wiki_pages_map = {}
779 project.wiki.pages.each do |page|
784 project.wiki.pages.each do |page|
780 # Skip pages without content
785 # Skip pages without content
781 next if page.content.nil?
786 next if page.content.nil?
782 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
787 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
783 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
788 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
784 new_wiki_page.content = new_wiki_content
789 new_wiki_page.content = new_wiki_content
785 wiki.pages << new_wiki_page
790 wiki.pages << new_wiki_page
786 wiki_pages_map[page.id] = new_wiki_page
791 wiki_pages_map[page.id] = new_wiki_page
787 end
792 end
788
793
789 self.wiki = wiki
794 self.wiki = wiki
790 wiki.save
795 wiki.save
791 # Reproduce page hierarchy
796 # Reproduce page hierarchy
792 project.wiki.pages.each do |page|
797 project.wiki.pages.each do |page|
793 if page.parent_id && wiki_pages_map[page.id]
798 if page.parent_id && wiki_pages_map[page.id]
794 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
799 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
795 wiki_pages_map[page.id].save
800 wiki_pages_map[page.id].save
796 end
801 end
797 end
802 end
798 end
803 end
799 end
804 end
800
805
801 # Copies versions from +project+
806 # Copies versions from +project+
802 def copy_versions(project)
807 def copy_versions(project)
803 project.versions.each do |version|
808 project.versions.each do |version|
804 new_version = Version.new
809 new_version = Version.new
805 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
810 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
806 self.versions << new_version
811 self.versions << new_version
807 end
812 end
808 end
813 end
809
814
810 # Copies issue categories from +project+
815 # Copies issue categories from +project+
811 def copy_issue_categories(project)
816 def copy_issue_categories(project)
812 project.issue_categories.each do |issue_category|
817 project.issue_categories.each do |issue_category|
813 new_issue_category = IssueCategory.new
818 new_issue_category = IssueCategory.new
814 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
819 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
815 self.issue_categories << new_issue_category
820 self.issue_categories << new_issue_category
816 end
821 end
817 end
822 end
818
823
819 # Copies issues from +project+
824 # Copies issues from +project+
820 def copy_issues(project)
825 def copy_issues(project)
821 # Stores the source issue id as a key and the copied issues as the
826 # Stores the source issue id as a key and the copied issues as the
822 # value. Used to map the two togeather for issue relations.
827 # value. Used to map the two togeather for issue relations.
823 issues_map = {}
828 issues_map = {}
824
829
825 # Store status and reopen locked/closed versions
830 # Store status and reopen locked/closed versions
826 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
831 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
827 version_statuses.each do |version, status|
832 version_statuses.each do |version, status|
828 version.update_attribute :status, 'open'
833 version.update_attribute :status, 'open'
829 end
834 end
830
835
831 # Get issues sorted by root_id, lft so that parent issues
836 # Get issues sorted by root_id, lft so that parent issues
832 # get copied before their children
837 # get copied before their children
833 project.issues.reorder('root_id, lft').all.each do |issue|
838 project.issues.reorder('root_id, lft').all.each do |issue|
834 new_issue = Issue.new
839 new_issue = Issue.new
835 new_issue.copy_from(issue, :subtasks => false, :link => false)
840 new_issue.copy_from(issue, :subtasks => false, :link => false)
836 new_issue.project = self
841 new_issue.project = self
837 # Reassign fixed_versions by name, since names are unique per project
842 # Reassign fixed_versions by name, since names are unique per project
838 if issue.fixed_version && issue.fixed_version.project == project
843 if issue.fixed_version && issue.fixed_version.project == project
839 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
844 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
840 end
845 end
841 # Reassign the category by name, since names are unique per project
846 # Reassign the category by name, since names are unique per project
842 if issue.category
847 if issue.category
843 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
848 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
844 end
849 end
845 # Parent issue
850 # Parent issue
846 if issue.parent_id
851 if issue.parent_id
847 if copied_parent = issues_map[issue.parent_id]
852 if copied_parent = issues_map[issue.parent_id]
848 new_issue.parent_issue_id = copied_parent.id
853 new_issue.parent_issue_id = copied_parent.id
849 end
854 end
850 end
855 end
851
856
852 self.issues << new_issue
857 self.issues << new_issue
853 if new_issue.new_record?
858 if new_issue.new_record?
854 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
859 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
855 else
860 else
856 issues_map[issue.id] = new_issue unless new_issue.new_record?
861 issues_map[issue.id] = new_issue unless new_issue.new_record?
857 end
862 end
858 end
863 end
859
864
860 # Restore locked/closed version statuses
865 # Restore locked/closed version statuses
861 version_statuses.each do |version, status|
866 version_statuses.each do |version, status|
862 version.update_attribute :status, status
867 version.update_attribute :status, status
863 end
868 end
864
869
865 # Relations after in case issues related each other
870 # Relations after in case issues related each other
866 project.issues.each do |issue|
871 project.issues.each do |issue|
867 new_issue = issues_map[issue.id]
872 new_issue = issues_map[issue.id]
868 unless new_issue
873 unless new_issue
869 # Issue was not copied
874 # Issue was not copied
870 next
875 next
871 end
876 end
872
877
873 # Relations
878 # Relations
874 issue.relations_from.each do |source_relation|
879 issue.relations_from.each do |source_relation|
875 new_issue_relation = IssueRelation.new
880 new_issue_relation = IssueRelation.new
876 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
881 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
877 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
882 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
878 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
883 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
879 new_issue_relation.issue_to = source_relation.issue_to
884 new_issue_relation.issue_to = source_relation.issue_to
880 end
885 end
881 new_issue.relations_from << new_issue_relation
886 new_issue.relations_from << new_issue_relation
882 end
887 end
883
888
884 issue.relations_to.each do |source_relation|
889 issue.relations_to.each do |source_relation|
885 new_issue_relation = IssueRelation.new
890 new_issue_relation = IssueRelation.new
886 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
891 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
887 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
892 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
888 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
893 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
889 new_issue_relation.issue_from = source_relation.issue_from
894 new_issue_relation.issue_from = source_relation.issue_from
890 end
895 end
891 new_issue.relations_to << new_issue_relation
896 new_issue.relations_to << new_issue_relation
892 end
897 end
893 end
898 end
894 end
899 end
895
900
896 # Copies members from +project+
901 # Copies members from +project+
897 def copy_members(project)
902 def copy_members(project)
898 # Copy users first, then groups to handle members with inherited and given roles
903 # Copy users first, then groups to handle members with inherited and given roles
899 members_to_copy = []
904 members_to_copy = []
900 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
905 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
901 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
906 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
902
907
903 members_to_copy.each do |member|
908 members_to_copy.each do |member|
904 new_member = Member.new
909 new_member = Member.new
905 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
910 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
906 # only copy non inherited roles
911 # only copy non inherited roles
907 # inherited roles will be added when copying the group membership
912 # inherited roles will be added when copying the group membership
908 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
913 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
909 next if role_ids.empty?
914 next if role_ids.empty?
910 new_member.role_ids = role_ids
915 new_member.role_ids = role_ids
911 new_member.project = self
916 new_member.project = self
912 self.members << new_member
917 self.members << new_member
913 end
918 end
914 end
919 end
915
920
916 # Copies queries from +project+
921 # Copies queries from +project+
917 def copy_queries(project)
922 def copy_queries(project)
918 project.queries.each do |query|
923 project.queries.each do |query|
919 new_query = IssueQuery.new
924 new_query = IssueQuery.new
920 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
925 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
921 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
926 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
922 new_query.project = self
927 new_query.project = self
923 new_query.user_id = query.user_id
928 new_query.user_id = query.user_id
924 self.queries << new_query
929 self.queries << new_query
925 end
930 end
926 end
931 end
927
932
928 # Copies boards from +project+
933 # Copies boards from +project+
929 def copy_boards(project)
934 def copy_boards(project)
930 project.boards.each do |board|
935 project.boards.each do |board|
931 new_board = Board.new
936 new_board = Board.new
932 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
937 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
933 new_board.project = self
938 new_board.project = self
934 self.boards << new_board
939 self.boards << new_board
935 end
940 end
936 end
941 end
937
942
938 def allowed_permissions
943 def allowed_permissions
939 @allowed_permissions ||= begin
944 @allowed_permissions ||= begin
940 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
945 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
941 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
946 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
942 end
947 end
943 end
948 end
944
949
945 def allowed_actions
950 def allowed_actions
946 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
951 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
947 end
952 end
948
953
949 # Returns all the active Systemwide and project specific activities
954 # Returns all the active Systemwide and project specific activities
950 def active_activities
955 def active_activities
951 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
956 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
952
957
953 if overridden_activity_ids.empty?
958 if overridden_activity_ids.empty?
954 return TimeEntryActivity.shared.active
959 return TimeEntryActivity.shared.active
955 else
960 else
956 return system_activities_and_project_overrides
961 return system_activities_and_project_overrides
957 end
962 end
958 end
963 end
959
964
960 # Returns all the Systemwide and project specific activities
965 # Returns all the Systemwide and project specific activities
961 # (inactive and active)
966 # (inactive and active)
962 def all_activities
967 def all_activities
963 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
968 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
964
969
965 if overridden_activity_ids.empty?
970 if overridden_activity_ids.empty?
966 return TimeEntryActivity.shared
971 return TimeEntryActivity.shared
967 else
972 else
968 return system_activities_and_project_overrides(true)
973 return system_activities_and_project_overrides(true)
969 end
974 end
970 end
975 end
971
976
972 # Returns the systemwide active activities merged with the project specific overrides
977 # Returns the systemwide active activities merged with the project specific overrides
973 def system_activities_and_project_overrides(include_inactive=false)
978 def system_activities_and_project_overrides(include_inactive=false)
974 if include_inactive
979 if include_inactive
975 return TimeEntryActivity.shared.
980 return TimeEntryActivity.shared.
976 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
981 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
977 self.time_entry_activities
982 self.time_entry_activities
978 else
983 else
979 return TimeEntryActivity.shared.active.
984 return TimeEntryActivity.shared.active.
980 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
985 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
981 self.time_entry_activities.active
986 self.time_entry_activities.active
982 end
987 end
983 end
988 end
984
989
985 # Archives subprojects recursively
990 # Archives subprojects recursively
986 def archive!
991 def archive!
987 children.each do |subproject|
992 children.each do |subproject|
988 subproject.send :archive!
993 subproject.send :archive!
989 end
994 end
990 update_attribute :status, STATUS_ARCHIVED
995 update_attribute :status, STATUS_ARCHIVED
991 end
996 end
992
997
993 def update_position_under_parent
998 def update_position_under_parent
994 set_or_update_position_under(parent)
999 set_or_update_position_under(parent)
995 end
1000 end
996
1001
997 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1002 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
998 def set_or_update_position_under(target_parent)
1003 def set_or_update_position_under(target_parent)
999 parent_was = parent
1004 parent_was = parent
1000 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1005 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1001 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1006 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1002
1007
1003 if to_be_inserted_before
1008 if to_be_inserted_before
1004 move_to_left_of(to_be_inserted_before)
1009 move_to_left_of(to_be_inserted_before)
1005 elsif target_parent.nil?
1010 elsif target_parent.nil?
1006 if sibs.empty?
1011 if sibs.empty?
1007 # move_to_root adds the project in first (ie. left) position
1012 # move_to_root adds the project in first (ie. left) position
1008 move_to_root
1013 move_to_root
1009 else
1014 else
1010 move_to_right_of(sibs.last) unless self == sibs.last
1015 move_to_right_of(sibs.last) unless self == sibs.last
1011 end
1016 end
1012 else
1017 else
1013 # move_to_child_of adds the project in last (ie.right) position
1018 # move_to_child_of adds the project in last (ie.right) position
1014 move_to_child_of(target_parent)
1019 move_to_child_of(target_parent)
1015 end
1020 end
1016 if parent_was != target_parent
1021 if parent_was != target_parent
1017 after_parent_changed(parent_was)
1022 after_parent_changed(parent_was)
1018 end
1023 end
1019 end
1024 end
1020 end
1025 end
@@ -1,17 +1,20
1 <%= form_tag({:action => 'edit', :tab => 'projects'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'projects'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :default_projects_public %></p>
4 <p><%= setting_check_box :default_projects_public %></p>
5
5
6 <p><%= setting_multiselect(:default_projects_modules,
6 <p><%= setting_multiselect(:default_projects_modules,
7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
8
8
9 <p><%= setting_multiselect(:default_projects_tracker_ids,
10 Tracker.sorted.all.collect {|t| [t.name, t.id.to_s]}) %></p>
11
9 <p><%= setting_check_box :sequential_project_identifiers %></p>
12 <p><%= setting_check_box :sequential_project_identifiers %></p>
10
13
11 <p><%= setting_select :new_project_user_role_id,
14 <p><%= setting_select :new_project_user_role_id,
12 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
15 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
13 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
16 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
14 </div>
17 </div>
15
18
16 <%= submit_tag l(:button_save) %>
19 <%= submit_tag l(:button_save) %>
17 <% end %>
20 <% end %>
@@ -1,229 +1,232
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18
18
19 # DO NOT MODIFY THIS FILE !!!
19 # DO NOT MODIFY THIS FILE !!!
20 # Settings can be defined through the application in Admin -> Settings
20 # Settings can be defined through the application in Admin -> Settings
21
21
22 app_title:
22 app_title:
23 default: Redmine
23 default: Redmine
24 app_subtitle:
24 app_subtitle:
25 default: Project management
25 default: Project management
26 welcome_text:
26 welcome_text:
27 default:
27 default:
28 login_required:
28 login_required:
29 default: 0
29 default: 0
30 self_registration:
30 self_registration:
31 default: '2'
31 default: '2'
32 lost_password:
32 lost_password:
33 default: 1
33 default: 1
34 unsubscribe:
34 unsubscribe:
35 default: 1
35 default: 1
36 password_min_length:
36 password_min_length:
37 format: int
37 format: int
38 default: 8
38 default: 8
39 # Maximum lifetime of user sessions in minutes
39 # Maximum lifetime of user sessions in minutes
40 session_lifetime:
40 session_lifetime:
41 format: int
41 format: int
42 default: 0
42 default: 0
43 # User session timeout in minutes
43 # User session timeout in minutes
44 session_timeout:
44 session_timeout:
45 format: int
45 format: int
46 default: 0
46 default: 0
47 attachment_max_size:
47 attachment_max_size:
48 format: int
48 format: int
49 default: 5120
49 default: 5120
50 issues_export_limit:
50 issues_export_limit:
51 format: int
51 format: int
52 default: 500
52 default: 500
53 activity_days_default:
53 activity_days_default:
54 format: int
54 format: int
55 default: 30
55 default: 30
56 per_page_options:
56 per_page_options:
57 default: '25,50,100'
57 default: '25,50,100'
58 mail_from:
58 mail_from:
59 default: redmine@example.net
59 default: redmine@example.net
60 bcc_recipients:
60 bcc_recipients:
61 default: 1
61 default: 1
62 plain_text_mail:
62 plain_text_mail:
63 default: 0
63 default: 0
64 text_formatting:
64 text_formatting:
65 default: textile
65 default: textile
66 cache_formatted_text:
66 cache_formatted_text:
67 default: 0
67 default: 0
68 wiki_compression:
68 wiki_compression:
69 default: ""
69 default: ""
70 default_language:
70 default_language:
71 default: en
71 default: en
72 host_name:
72 host_name:
73 default: localhost:3000
73 default: localhost:3000
74 protocol:
74 protocol:
75 default: http
75 default: http
76 feeds_limit:
76 feeds_limit:
77 format: int
77 format: int
78 default: 15
78 default: 15
79 gantt_items_limit:
79 gantt_items_limit:
80 format: int
80 format: int
81 default: 500
81 default: 500
82 # Maximum size of files that can be displayed
82 # Maximum size of files that can be displayed
83 # inline through the file viewer (in KB)
83 # inline through the file viewer (in KB)
84 file_max_size_displayed:
84 file_max_size_displayed:
85 format: int
85 format: int
86 default: 512
86 default: 512
87 diff_max_lines_displayed:
87 diff_max_lines_displayed:
88 format: int
88 format: int
89 default: 1500
89 default: 1500
90 enabled_scm:
90 enabled_scm:
91 serialized: true
91 serialized: true
92 default:
92 default:
93 - Subversion
93 - Subversion
94 - Darcs
94 - Darcs
95 - Mercurial
95 - Mercurial
96 - Cvs
96 - Cvs
97 - Bazaar
97 - Bazaar
98 - Git
98 - Git
99 autofetch_changesets:
99 autofetch_changesets:
100 default: 1
100 default: 1
101 sys_api_enabled:
101 sys_api_enabled:
102 default: 0
102 default: 0
103 sys_api_key:
103 sys_api_key:
104 default: ''
104 default: ''
105 commit_cross_project_ref:
105 commit_cross_project_ref:
106 default: 0
106 default: 0
107 commit_ref_keywords:
107 commit_ref_keywords:
108 default: 'refs,references,IssueID'
108 default: 'refs,references,IssueID'
109 commit_fix_keywords:
109 commit_fix_keywords:
110 default: 'fixes,closes'
110 default: 'fixes,closes'
111 commit_fix_status_id:
111 commit_fix_status_id:
112 format: int
112 format: int
113 default: 0
113 default: 0
114 commit_fix_done_ratio:
114 commit_fix_done_ratio:
115 default: 100
115 default: 100
116 commit_logtime_enabled:
116 commit_logtime_enabled:
117 default: 0
117 default: 0
118 commit_logtime_activity_id:
118 commit_logtime_activity_id:
119 format: int
119 format: int
120 default: 0
120 default: 0
121 # autologin duration in days
121 # autologin duration in days
122 # 0 means autologin is disabled
122 # 0 means autologin is disabled
123 autologin:
123 autologin:
124 format: int
124 format: int
125 default: 0
125 default: 0
126 # date format
126 # date format
127 date_format:
127 date_format:
128 default: ''
128 default: ''
129 time_format:
129 time_format:
130 default: ''
130 default: ''
131 user_format:
131 user_format:
132 default: :firstname_lastname
132 default: :firstname_lastname
133 format: symbol
133 format: symbol
134 cross_project_issue_relations:
134 cross_project_issue_relations:
135 default: 0
135 default: 0
136 # Enables subtasks to be in other projects
136 # Enables subtasks to be in other projects
137 cross_project_subtasks:
137 cross_project_subtasks:
138 default: 'tree'
138 default: 'tree'
139 issue_group_assignment:
139 issue_group_assignment:
140 default: 0
140 default: 0
141 default_issue_start_date_to_creation_date:
141 default_issue_start_date_to_creation_date:
142 default: 1
142 default: 1
143 notified_events:
143 notified_events:
144 serialized: true
144 serialized: true
145 default:
145 default:
146 - issue_added
146 - issue_added
147 - issue_updated
147 - issue_updated
148 mail_handler_body_delimiters:
148 mail_handler_body_delimiters:
149 default: ''
149 default: ''
150 mail_handler_api_enabled:
150 mail_handler_api_enabled:
151 default: 0
151 default: 0
152 mail_handler_api_key:
152 mail_handler_api_key:
153 default:
153 default:
154 issue_list_default_columns:
154 issue_list_default_columns:
155 serialized: true
155 serialized: true
156 default:
156 default:
157 - tracker
157 - tracker
158 - status
158 - status
159 - priority
159 - priority
160 - subject
160 - subject
161 - assigned_to
161 - assigned_to
162 - updated_on
162 - updated_on
163 display_subprojects_issues:
163 display_subprojects_issues:
164 default: 1
164 default: 1
165 issue_done_ratio:
165 issue_done_ratio:
166 default: 'issue_field'
166 default: 'issue_field'
167 default_projects_public:
167 default_projects_public:
168 default: 1
168 default: 1
169 default_projects_modules:
169 default_projects_modules:
170 serialized: true
170 serialized: true
171 default:
171 default:
172 - issue_tracking
172 - issue_tracking
173 - time_tracking
173 - time_tracking
174 - news
174 - news
175 - documents
175 - documents
176 - files
176 - files
177 - wiki
177 - wiki
178 - repository
178 - repository
179 - boards
179 - boards
180 - calendar
180 - calendar
181 - gantt
181 - gantt
182 default_projects_tracker_ids:
183 serialized: true
184 default:
182 # Role given to a non-admin user who creates a project
185 # Role given to a non-admin user who creates a project
183 new_project_user_role_id:
186 new_project_user_role_id:
184 format: int
187 format: int
185 default: ''
188 default: ''
186 sequential_project_identifiers:
189 sequential_project_identifiers:
187 default: 0
190 default: 0
188 # encodings used to convert repository files content to UTF-8
191 # encodings used to convert repository files content to UTF-8
189 # multiple values accepted, comma separated
192 # multiple values accepted, comma separated
190 repositories_encodings:
193 repositories_encodings:
191 default: ''
194 default: ''
192 # encoding used to convert commit logs to UTF-8
195 # encoding used to convert commit logs to UTF-8
193 commit_logs_encoding:
196 commit_logs_encoding:
194 default: 'UTF-8'
197 default: 'UTF-8'
195 repository_log_display_limit:
198 repository_log_display_limit:
196 format: int
199 format: int
197 default: 100
200 default: 100
198 ui_theme:
201 ui_theme:
199 default: ''
202 default: ''
200 emails_footer:
203 emails_footer:
201 default: |-
204 default: |-
202 You have received this notification because you have either subscribed to it, or are involved in it.
205 You have received this notification because you have either subscribed to it, or are involved in it.
203 To change your notification preferences, please click here: http://hostname/my/account
206 To change your notification preferences, please click here: http://hostname/my/account
204 gravatar_enabled:
207 gravatar_enabled:
205 default: 0
208 default: 0
206 openid:
209 openid:
207 default: 0
210 default: 0
208 gravatar_default:
211 gravatar_default:
209 default: ''
212 default: ''
210 start_of_week:
213 start_of_week:
211 default: ''
214 default: ''
212 rest_api_enabled:
215 rest_api_enabled:
213 default: 0
216 default: 0
214 jsonp_enabled:
217 jsonp_enabled:
215 default: 0
218 default: 0
216 default_notification_option:
219 default_notification_option:
217 default: 'only_my_events'
220 default: 'only_my_events'
218 emails_header:
221 emails_header:
219 default: ''
222 default: ''
220 thumbnails_enabled:
223 thumbnails_enabled:
221 default: 0
224 default: 0
222 thumbnails_size:
225 thumbnails_size:
223 format: int
226 format: int
224 default: 100
227 default: 100
225 non_working_week_days:
228 non_working_week_days:
226 serialized: true
229 serialized: true
227 default:
230 default:
228 - '6'
231 - '6'
229 - '7'
232 - '7'
@@ -1,460 +1,468
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 #require 'shoulda'
18 #require 'shoulda'
19 ENV["RAILS_ENV"] = "test"
19 ENV["RAILS_ENV"] = "test"
20 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
21 require 'rails/test_help'
21 require 'rails/test_help'
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
23
23
24 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
24 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
25 include ObjectHelpers
25 include ObjectHelpers
26
26
27 class ActiveSupport::TestCase
27 class ActiveSupport::TestCase
28 include ActionDispatch::TestProcess
28 include ActionDispatch::TestProcess
29
29
30 self.use_transactional_fixtures = true
30 self.use_transactional_fixtures = true
31 self.use_instantiated_fixtures = false
31 self.use_instantiated_fixtures = false
32
32
33 def log_user(login, password)
33 def log_user(login, password)
34 User.anonymous
34 User.anonymous
35 get "/login"
35 get "/login"
36 assert_equal nil, session[:user_id]
36 assert_equal nil, session[:user_id]
37 assert_response :success
37 assert_response :success
38 assert_template "account/login"
38 assert_template "account/login"
39 post "/login", :username => login, :password => password
39 post "/login", :username => login, :password => password
40 assert_equal login, User.find(session[:user_id]).login
40 assert_equal login, User.find(session[:user_id]).login
41 end
41 end
42
42
43 def uploaded_test_file(name, mime)
43 def uploaded_test_file(name, mime)
44 fixture_file_upload("files/#{name}", mime, true)
44 fixture_file_upload("files/#{name}", mime, true)
45 end
45 end
46
46
47 def credentials(user, password=nil)
47 def credentials(user, password=nil)
48 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
48 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
49 end
49 end
50
50
51 # Mock out a file
51 # Mock out a file
52 def self.mock_file
52 def self.mock_file
53 file = 'a_file.png'
53 file = 'a_file.png'
54 file.stubs(:size).returns(32)
54 file.stubs(:size).returns(32)
55 file.stubs(:original_filename).returns('a_file.png')
55 file.stubs(:original_filename).returns('a_file.png')
56 file.stubs(:content_type).returns('image/png')
56 file.stubs(:content_type).returns('image/png')
57 file.stubs(:read).returns(false)
57 file.stubs(:read).returns(false)
58 file
58 file
59 end
59 end
60
60
61 def mock_file
61 def mock_file
62 self.class.mock_file
62 self.class.mock_file
63 end
63 end
64
64
65 def mock_file_with_options(options={})
65 def mock_file_with_options(options={})
66 file = ''
66 file = ''
67 file.stubs(:size).returns(32)
67 file.stubs(:size).returns(32)
68 original_filename = options[:original_filename] || nil
68 original_filename = options[:original_filename] || nil
69 file.stubs(:original_filename).returns(original_filename)
69 file.stubs(:original_filename).returns(original_filename)
70 content_type = options[:content_type] || nil
70 content_type = options[:content_type] || nil
71 file.stubs(:content_type).returns(content_type)
71 file.stubs(:content_type).returns(content_type)
72 file.stubs(:read).returns(false)
72 file.stubs(:read).returns(false)
73 file
73 file
74 end
74 end
75
75
76 # Use a temporary directory for attachment related tests
76 # Use a temporary directory for attachment related tests
77 def set_tmp_attachments_directory
77 def set_tmp_attachments_directory
78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
81 end
81 end
82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
83 end
83 end
84
84
85 def set_fixtures_attachments_directory
85 def set_fixtures_attachments_directory
86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
87 end
87 end
88
88
89 def with_settings(options, &block)
89 def with_settings(options, &block)
90 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h}
90 saved_settings = options.keys.inject({}) do |h, k|
91 h[k] = case Setting[k]
92 when Symbol, false, true, nil
93 Setting[k]
94 else
95 Setting[k].dup
96 end
97 h
98 end
91 options.each {|k, v| Setting[k] = v}
99 options.each {|k, v| Setting[k] = v}
92 yield
100 yield
93 ensure
101 ensure
94 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
102 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
95 end
103 end
96
104
97 # Yields the block with user as the current user
105 # Yields the block with user as the current user
98 def with_current_user(user, &block)
106 def with_current_user(user, &block)
99 saved_user = User.current
107 saved_user = User.current
100 User.current = user
108 User.current = user
101 yield
109 yield
102 ensure
110 ensure
103 User.current = saved_user
111 User.current = saved_user
104 end
112 end
105
113
106 def change_user_password(login, new_password)
114 def change_user_password(login, new_password)
107 user = User.first(:conditions => {:login => login})
115 user = User.first(:conditions => {:login => login})
108 user.password, user.password_confirmation = new_password, new_password
116 user.password, user.password_confirmation = new_password, new_password
109 user.save!
117 user.save!
110 end
118 end
111
119
112 def self.ldap_configured?
120 def self.ldap_configured?
113 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
121 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
114 return @test_ldap.bind
122 return @test_ldap.bind
115 rescue Exception => e
123 rescue Exception => e
116 # LDAP is not listening
124 # LDAP is not listening
117 return nil
125 return nil
118 end
126 end
119
127
120 def self.convert_installed?
128 def self.convert_installed?
121 Redmine::Thumbnail.convert_available?
129 Redmine::Thumbnail.convert_available?
122 end
130 end
123
131
124 # Returns the path to the test +vendor+ repository
132 # Returns the path to the test +vendor+ repository
125 def self.repository_path(vendor)
133 def self.repository_path(vendor)
126 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
134 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
127 end
135 end
128
136
129 # Returns the url of the subversion test repository
137 # Returns the url of the subversion test repository
130 def self.subversion_repository_url
138 def self.subversion_repository_url
131 path = repository_path('subversion')
139 path = repository_path('subversion')
132 path = '/' + path unless path.starts_with?('/')
140 path = '/' + path unless path.starts_with?('/')
133 "file://#{path}"
141 "file://#{path}"
134 end
142 end
135
143
136 # Returns true if the +vendor+ test repository is configured
144 # Returns true if the +vendor+ test repository is configured
137 def self.repository_configured?(vendor)
145 def self.repository_configured?(vendor)
138 File.directory?(repository_path(vendor))
146 File.directory?(repository_path(vendor))
139 end
147 end
140
148
141 def repository_path_hash(arr)
149 def repository_path_hash(arr)
142 hs = {}
150 hs = {}
143 hs[:path] = arr.join("/")
151 hs[:path] = arr.join("/")
144 hs[:param] = arr.join("/")
152 hs[:param] = arr.join("/")
145 hs
153 hs
146 end
154 end
147
155
148 def assert_save(object)
156 def assert_save(object)
149 saved = object.save
157 saved = object.save
150 message = "#{object.class} could not be saved"
158 message = "#{object.class} could not be saved"
151 errors = object.errors.full_messages.map {|m| "- #{m}"}
159 errors = object.errors.full_messages.map {|m| "- #{m}"}
152 message << ":\n#{errors.join("\n")}" if errors.any?
160 message << ":\n#{errors.join("\n")}" if errors.any?
153 assert_equal true, saved, message
161 assert_equal true, saved, message
154 end
162 end
155
163
156 def assert_error_tag(options={})
164 def assert_error_tag(options={})
157 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
165 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
158 end
166 end
159
167
160 def assert_include(expected, s, message=nil)
168 def assert_include(expected, s, message=nil)
161 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
169 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
162 end
170 end
163
171
164 def assert_not_include(expected, s)
172 def assert_not_include(expected, s)
165 assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
173 assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
166 end
174 end
167
175
168 def assert_select_in(text, *args, &block)
176 def assert_select_in(text, *args, &block)
169 d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root
177 d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root
170 assert_select(d, *args, &block)
178 assert_select(d, *args, &block)
171 end
179 end
172
180
173 def assert_mail_body_match(expected, mail)
181 def assert_mail_body_match(expected, mail)
174 if expected.is_a?(String)
182 if expected.is_a?(String)
175 assert_include expected, mail_body(mail)
183 assert_include expected, mail_body(mail)
176 else
184 else
177 assert_match expected, mail_body(mail)
185 assert_match expected, mail_body(mail)
178 end
186 end
179 end
187 end
180
188
181 def assert_mail_body_no_match(expected, mail)
189 def assert_mail_body_no_match(expected, mail)
182 if expected.is_a?(String)
190 if expected.is_a?(String)
183 assert_not_include expected, mail_body(mail)
191 assert_not_include expected, mail_body(mail)
184 else
192 else
185 assert_no_match expected, mail_body(mail)
193 assert_no_match expected, mail_body(mail)
186 end
194 end
187 end
195 end
188
196
189 def mail_body(mail)
197 def mail_body(mail)
190 mail.parts.first.body.encoded
198 mail.parts.first.body.encoded
191 end
199 end
192 end
200 end
193
201
194 module Redmine
202 module Redmine
195 module ApiTest
203 module ApiTest
196 # Base class for API tests
204 # Base class for API tests
197 class Base < ActionDispatch::IntegrationTest
205 class Base < ActionDispatch::IntegrationTest
198 # Test that a request allows the three types of API authentication
206 # Test that a request allows the three types of API authentication
199 #
207 #
200 # * HTTP Basic with username and password
208 # * HTTP Basic with username and password
201 # * HTTP Basic with an api key for the username
209 # * HTTP Basic with an api key for the username
202 # * Key based with the key=X parameter
210 # * Key based with the key=X parameter
203 #
211 #
204 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
212 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
205 # @param [String] url the request url
213 # @param [String] url the request url
206 # @param [optional, Hash] parameters additional request parameters
214 # @param [optional, Hash] parameters additional request parameters
207 # @param [optional, Hash] options additional options
215 # @param [optional, Hash] options additional options
208 # @option options [Symbol] :success_code Successful response code (:success)
216 # @option options [Symbol] :success_code Successful response code (:success)
209 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
217 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
210 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
218 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
211 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
219 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
212 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
220 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
213 should_allow_key_based_auth(http_method, url, parameters, options)
221 should_allow_key_based_auth(http_method, url, parameters, options)
214 end
222 end
215
223
216 # Test that a request allows the username and password for HTTP BASIC
224 # Test that a request allows the username and password for HTTP BASIC
217 #
225 #
218 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
226 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
219 # @param [String] url the request url
227 # @param [String] url the request url
220 # @param [optional, Hash] parameters additional request parameters
228 # @param [optional, Hash] parameters additional request parameters
221 # @param [optional, Hash] options additional options
229 # @param [optional, Hash] options additional options
222 # @option options [Symbol] :success_code Successful response code (:success)
230 # @option options [Symbol] :success_code Successful response code (:success)
223 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
231 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
224 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
232 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
225 success_code = options[:success_code] || :success
233 success_code = options[:success_code] || :success
226 failure_code = options[:failure_code] || :unauthorized
234 failure_code = options[:failure_code] || :unauthorized
227
235
228 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
236 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
229 context "with a valid HTTP authentication" do
237 context "with a valid HTTP authentication" do
230 setup do
238 setup do
231 @user = User.generate! do |user|
239 @user = User.generate! do |user|
232 user.admin = true
240 user.admin = true
233 user.password = 'my_password'
241 user.password = 'my_password'
234 end
242 end
235 send(http_method, url, parameters, credentials(@user.login, 'my_password'))
243 send(http_method, url, parameters, credentials(@user.login, 'my_password'))
236 end
244 end
237
245
238 should_respond_with success_code
246 should_respond_with success_code
239 should_respond_with_content_type_based_on_url(url)
247 should_respond_with_content_type_based_on_url(url)
240 should "login as the user" do
248 should "login as the user" do
241 assert_equal @user, User.current
249 assert_equal @user, User.current
242 end
250 end
243 end
251 end
244
252
245 context "with an invalid HTTP authentication" do
253 context "with an invalid HTTP authentication" do
246 setup do
254 setup do
247 @user = User.generate!
255 @user = User.generate!
248 send(http_method, url, parameters, credentials(@user.login, 'wrong_password'))
256 send(http_method, url, parameters, credentials(@user.login, 'wrong_password'))
249 end
257 end
250
258
251 should_respond_with failure_code
259 should_respond_with failure_code
252 should_respond_with_content_type_based_on_url(url)
260 should_respond_with_content_type_based_on_url(url)
253 should "not login as the user" do
261 should "not login as the user" do
254 assert_equal User.anonymous, User.current
262 assert_equal User.anonymous, User.current
255 end
263 end
256 end
264 end
257
265
258 context "without credentials" do
266 context "without credentials" do
259 setup do
267 setup do
260 send(http_method, url, parameters)
268 send(http_method, url, parameters)
261 end
269 end
262
270
263 should_respond_with failure_code
271 should_respond_with failure_code
264 should_respond_with_content_type_based_on_url(url)
272 should_respond_with_content_type_based_on_url(url)
265 should "include_www_authenticate_header" do
273 should "include_www_authenticate_header" do
266 assert @controller.response.headers.has_key?('WWW-Authenticate')
274 assert @controller.response.headers.has_key?('WWW-Authenticate')
267 end
275 end
268 end
276 end
269 end
277 end
270 end
278 end
271
279
272 # Test that a request allows the API key with HTTP BASIC
280 # Test that a request allows the API key with HTTP BASIC
273 #
281 #
274 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
282 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
275 # @param [String] url the request url
283 # @param [String] url the request url
276 # @param [optional, Hash] parameters additional request parameters
284 # @param [optional, Hash] parameters additional request parameters
277 # @param [optional, Hash] options additional options
285 # @param [optional, Hash] options additional options
278 # @option options [Symbol] :success_code Successful response code (:success)
286 # @option options [Symbol] :success_code Successful response code (:success)
279 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
287 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
280 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
288 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
281 success_code = options[:success_code] || :success
289 success_code = options[:success_code] || :success
282 failure_code = options[:failure_code] || :unauthorized
290 failure_code = options[:failure_code] || :unauthorized
283
291
284 context "should allow http basic auth with a key for #{http_method} #{url}" do
292 context "should allow http basic auth with a key for #{http_method} #{url}" do
285 context "with a valid HTTP authentication using the API token" do
293 context "with a valid HTTP authentication using the API token" do
286 setup do
294 setup do
287 @user = User.generate! do |user|
295 @user = User.generate! do |user|
288 user.admin = true
296 user.admin = true
289 end
297 end
290 @token = Token.create!(:user => @user, :action => 'api')
298 @token = Token.create!(:user => @user, :action => 'api')
291 send(http_method, url, parameters, credentials(@token.value, 'X'))
299 send(http_method, url, parameters, credentials(@token.value, 'X'))
292 end
300 end
293 should_respond_with success_code
301 should_respond_with success_code
294 should_respond_with_content_type_based_on_url(url)
302 should_respond_with_content_type_based_on_url(url)
295 should_be_a_valid_response_string_based_on_url(url)
303 should_be_a_valid_response_string_based_on_url(url)
296 should "login as the user" do
304 should "login as the user" do
297 assert_equal @user, User.current
305 assert_equal @user, User.current
298 end
306 end
299 end
307 end
300
308
301 context "with an invalid HTTP authentication" do
309 context "with an invalid HTTP authentication" do
302 setup do
310 setup do
303 @user = User.generate!
311 @user = User.generate!
304 @token = Token.create!(:user => @user, :action => 'feeds')
312 @token = Token.create!(:user => @user, :action => 'feeds')
305 send(http_method, url, parameters, credentials(@token.value, 'X'))
313 send(http_method, url, parameters, credentials(@token.value, 'X'))
306 end
314 end
307 should_respond_with failure_code
315 should_respond_with failure_code
308 should_respond_with_content_type_based_on_url(url)
316 should_respond_with_content_type_based_on_url(url)
309 should "not login as the user" do
317 should "not login as the user" do
310 assert_equal User.anonymous, User.current
318 assert_equal User.anonymous, User.current
311 end
319 end
312 end
320 end
313 end
321 end
314 end
322 end
315
323
316 # Test that a request allows full key authentication
324 # Test that a request allows full key authentication
317 #
325 #
318 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
326 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
319 # @param [String] url the request url, without the key=ZXY parameter
327 # @param [String] url the request url, without the key=ZXY parameter
320 # @param [optional, Hash] parameters additional request parameters
328 # @param [optional, Hash] parameters additional request parameters
321 # @param [optional, Hash] options additional options
329 # @param [optional, Hash] options additional options
322 # @option options [Symbol] :success_code Successful response code (:success)
330 # @option options [Symbol] :success_code Successful response code (:success)
323 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
331 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
324 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
332 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
325 success_code = options[:success_code] || :success
333 success_code = options[:success_code] || :success
326 failure_code = options[:failure_code] || :unauthorized
334 failure_code = options[:failure_code] || :unauthorized
327
335
328 context "should allow key based auth using key=X for #{http_method} #{url}" do
336 context "should allow key based auth using key=X for #{http_method} #{url}" do
329 context "with a valid api token" do
337 context "with a valid api token" do
330 setup do
338 setup do
331 @user = User.generate! do |user|
339 @user = User.generate! do |user|
332 user.admin = true
340 user.admin = true
333 end
341 end
334 @token = Token.create!(:user => @user, :action => 'api')
342 @token = Token.create!(:user => @user, :action => 'api')
335 # Simple url parse to add on ?key= or &key=
343 # Simple url parse to add on ?key= or &key=
336 request_url = if url.match(/\?/)
344 request_url = if url.match(/\?/)
337 url + "&key=#{@token.value}"
345 url + "&key=#{@token.value}"
338 else
346 else
339 url + "?key=#{@token.value}"
347 url + "?key=#{@token.value}"
340 end
348 end
341 send(http_method, request_url, parameters)
349 send(http_method, request_url, parameters)
342 end
350 end
343 should_respond_with success_code
351 should_respond_with success_code
344 should_respond_with_content_type_based_on_url(url)
352 should_respond_with_content_type_based_on_url(url)
345 should_be_a_valid_response_string_based_on_url(url)
353 should_be_a_valid_response_string_based_on_url(url)
346 should "login as the user" do
354 should "login as the user" do
347 assert_equal @user, User.current
355 assert_equal @user, User.current
348 end
356 end
349 end
357 end
350
358
351 context "with an invalid api token" do
359 context "with an invalid api token" do
352 setup do
360 setup do
353 @user = User.generate! do |user|
361 @user = User.generate! do |user|
354 user.admin = true
362 user.admin = true
355 end
363 end
356 @token = Token.create!(:user => @user, :action => 'feeds')
364 @token = Token.create!(:user => @user, :action => 'feeds')
357 # Simple url parse to add on ?key= or &key=
365 # Simple url parse to add on ?key= or &key=
358 request_url = if url.match(/\?/)
366 request_url = if url.match(/\?/)
359 url + "&key=#{@token.value}"
367 url + "&key=#{@token.value}"
360 else
368 else
361 url + "?key=#{@token.value}"
369 url + "?key=#{@token.value}"
362 end
370 end
363 send(http_method, request_url, parameters)
371 send(http_method, request_url, parameters)
364 end
372 end
365 should_respond_with failure_code
373 should_respond_with failure_code
366 should_respond_with_content_type_based_on_url(url)
374 should_respond_with_content_type_based_on_url(url)
367 should "not login as the user" do
375 should "not login as the user" do
368 assert_equal User.anonymous, User.current
376 assert_equal User.anonymous, User.current
369 end
377 end
370 end
378 end
371 end
379 end
372
380
373 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
381 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
374 setup do
382 setup do
375 @user = User.generate! do |user|
383 @user = User.generate! do |user|
376 user.admin = true
384 user.admin = true
377 end
385 end
378 @token = Token.create!(:user => @user, :action => 'api')
386 @token = Token.create!(:user => @user, :action => 'api')
379 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
387 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
380 end
388 end
381 should_respond_with success_code
389 should_respond_with success_code
382 should_respond_with_content_type_based_on_url(url)
390 should_respond_with_content_type_based_on_url(url)
383 should_be_a_valid_response_string_based_on_url(url)
391 should_be_a_valid_response_string_based_on_url(url)
384 should "login as the user" do
392 should "login as the user" do
385 assert_equal @user, User.current
393 assert_equal @user, User.current
386 end
394 end
387 end
395 end
388 end
396 end
389
397
390 # Uses should_respond_with_content_type based on what's in the url:
398 # Uses should_respond_with_content_type based on what's in the url:
391 #
399 #
392 # '/project/issues.xml' => should_respond_with_content_type :xml
400 # '/project/issues.xml' => should_respond_with_content_type :xml
393 # '/project/issues.json' => should_respond_with_content_type :json
401 # '/project/issues.json' => should_respond_with_content_type :json
394 #
402 #
395 # @param [String] url Request
403 # @param [String] url Request
396 def self.should_respond_with_content_type_based_on_url(url)
404 def self.should_respond_with_content_type_based_on_url(url)
397 case
405 case
398 when url.match(/xml/i)
406 when url.match(/xml/i)
399 should "respond with XML" do
407 should "respond with XML" do
400 assert_equal 'application/xml', @response.content_type
408 assert_equal 'application/xml', @response.content_type
401 end
409 end
402 when url.match(/json/i)
410 when url.match(/json/i)
403 should "respond with JSON" do
411 should "respond with JSON" do
404 assert_equal 'application/json', @response.content_type
412 assert_equal 'application/json', @response.content_type
405 end
413 end
406 else
414 else
407 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
415 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
408 end
416 end
409 end
417 end
410
418
411 # Uses the url to assert which format the response should be in
419 # Uses the url to assert which format the response should be in
412 #
420 #
413 # '/project/issues.xml' => should_be_a_valid_xml_string
421 # '/project/issues.xml' => should_be_a_valid_xml_string
414 # '/project/issues.json' => should_be_a_valid_json_string
422 # '/project/issues.json' => should_be_a_valid_json_string
415 #
423 #
416 # @param [String] url Request
424 # @param [String] url Request
417 def self.should_be_a_valid_response_string_based_on_url(url)
425 def self.should_be_a_valid_response_string_based_on_url(url)
418 case
426 case
419 when url.match(/xml/i)
427 when url.match(/xml/i)
420 should_be_a_valid_xml_string
428 should_be_a_valid_xml_string
421 when url.match(/json/i)
429 when url.match(/json/i)
422 should_be_a_valid_json_string
430 should_be_a_valid_json_string
423 else
431 else
424 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
432 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
425 end
433 end
426 end
434 end
427
435
428 # Checks that the response is a valid JSON string
436 # Checks that the response is a valid JSON string
429 def self.should_be_a_valid_json_string
437 def self.should_be_a_valid_json_string
430 should "be a valid JSON string (or empty)" do
438 should "be a valid JSON string (or empty)" do
431 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
439 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
432 end
440 end
433 end
441 end
434
442
435 # Checks that the response is a valid XML string
443 # Checks that the response is a valid XML string
436 def self.should_be_a_valid_xml_string
444 def self.should_be_a_valid_xml_string
437 should "be a valid XML string" do
445 should "be a valid XML string" do
438 assert REXML::Document.new(response.body)
446 assert REXML::Document.new(response.body)
439 end
447 end
440 end
448 end
441
449
442 def self.should_respond_with(status)
450 def self.should_respond_with(status)
443 should "respond with #{status}" do
451 should "respond with #{status}" do
444 assert_response status
452 assert_response status
445 end
453 end
446 end
454 end
447 end
455 end
448 end
456 end
449 end
457 end
450
458
451 # URL helpers do not work with config.threadsafe!
459 # URL helpers do not work with config.threadsafe!
452 # https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454
460 # https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454
453 ActionView::TestCase::TestController.instance_eval do
461 ActionView::TestCase::TestController.instance_eval do
454 helper Rails.application.routes.url_helpers
462 helper Rails.application.routes.url_helpers
455 end
463 end
456 ActionView::TestCase::TestController.class_eval do
464 ActionView::TestCase::TestController.class_eval do
457 def _routes
465 def _routes
458 Rails.application.routes
466 Rails.application.routes
459 end
467 end
460 end
468 end
@@ -1,916 +1,937
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :versions,
33 :versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :groups_users,
35 :groups_users,
36 :boards, :messages,
36 :boards, :messages,
37 :repositories,
37 :repositories,
38 :news, :comments,
38 :news, :comments,
39 :documents
39 :documents
40
40
41 def setup
41 def setup
42 @ecookbook = Project.find(1)
42 @ecookbook = Project.find(1)
43 @ecookbook_sub1 = Project.find(3)
43 @ecookbook_sub1 = Project.find(3)
44 set_tmp_attachments_directory
44 set_tmp_attachments_directory
45 User.current = nil
45 User.current = nil
46 end
46 end
47
47
48 def test_truth
48 def test_truth
49 assert_kind_of Project, @ecookbook
49 assert_kind_of Project, @ecookbook
50 assert_equal "eCookbook", @ecookbook.name
50 assert_equal "eCookbook", @ecookbook.name
51 end
51 end
52
52
53 def test_default_attributes
53 def test_default_attributes
54 with_settings :default_projects_public => '1' do
54 with_settings :default_projects_public => '1' do
55 assert_equal true, Project.new.is_public
55 assert_equal true, Project.new.is_public
56 assert_equal false, Project.new(:is_public => false).is_public
56 assert_equal false, Project.new(:is_public => false).is_public
57 end
57 end
58
58
59 with_settings :default_projects_public => '0' do
59 with_settings :default_projects_public => '0' do
60 assert_equal false, Project.new.is_public
60 assert_equal false, Project.new.is_public
61 assert_equal true, Project.new(:is_public => true).is_public
61 assert_equal true, Project.new(:is_public => true).is_public
62 end
62 end
63
63
64 with_settings :sequential_project_identifiers => '1' do
64 with_settings :sequential_project_identifiers => '1' do
65 assert !Project.new.identifier.blank?
65 assert !Project.new.identifier.blank?
66 assert Project.new(:identifier => '').identifier.blank?
66 assert Project.new(:identifier => '').identifier.blank?
67 end
67 end
68
68
69 with_settings :sequential_project_identifiers => '0' do
69 with_settings :sequential_project_identifiers => '0' do
70 assert Project.new.identifier.blank?
70 assert Project.new.identifier.blank?
71 assert !Project.new(:identifier => 'test').blank?
71 assert !Project.new(:identifier => 'test').blank?
72 end
72 end
73
73
74 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
74 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
75 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
75 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
76 end
76 end
77 end
78
79 def test_default_trackers_should_match_default_tracker_ids_setting
80 with_settings :default_projects_tracker_ids => ['1', '3'] do
81 assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort
82 end
83 end
77
84
85 def test_default_trackers_should_be_all_trackers_with_blank_setting
86 with_settings :default_projects_tracker_ids => nil do
78 assert_equal Tracker.all.sort, Project.new.trackers.sort
87 assert_equal Tracker.all.sort, Project.new.trackers.sort
79 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
88 end
89 end
90
91 def test_default_trackers_should_be_empty_with_empty_setting
92 with_settings :default_projects_tracker_ids => [] do
93 assert_equal [], Project.new.trackers
94 end
95 end
96
97 def test_default_trackers_should_not_replace_initialized_trackers
98 with_settings :default_projects_tracker_ids => ['1', '3'] do
99 assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort
100 end
80 end
101 end
81
102
82 def test_update
103 def test_update
83 assert_equal "eCookbook", @ecookbook.name
104 assert_equal "eCookbook", @ecookbook.name
84 @ecookbook.name = "eCook"
105 @ecookbook.name = "eCook"
85 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
106 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
86 @ecookbook.reload
107 @ecookbook.reload
87 assert_equal "eCook", @ecookbook.name
108 assert_equal "eCook", @ecookbook.name
88 end
109 end
89
110
90 def test_validate_identifier
111 def test_validate_identifier
91 to_test = {"abc" => true,
112 to_test = {"abc" => true,
92 "ab12" => true,
113 "ab12" => true,
93 "ab-12" => true,
114 "ab-12" => true,
94 "ab_12" => true,
115 "ab_12" => true,
95 "12" => false,
116 "12" => false,
96 "new" => false}
117 "new" => false}
97
118
98 to_test.each do |identifier, valid|
119 to_test.each do |identifier, valid|
99 p = Project.new
120 p = Project.new
100 p.identifier = identifier
121 p.identifier = identifier
101 p.valid?
122 p.valid?
102 if valid
123 if valid
103 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
124 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
104 else
125 else
105 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
126 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
106 end
127 end
107 end
128 end
108 end
129 end
109
130
110 def test_identifier_should_not_be_frozen_for_a_new_project
131 def test_identifier_should_not_be_frozen_for_a_new_project
111 assert_equal false, Project.new.identifier_frozen?
132 assert_equal false, Project.new.identifier_frozen?
112 end
133 end
113
134
114 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
135 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
115 Project.update_all(["identifier = ''"], "id = 1")
136 Project.update_all(["identifier = ''"], "id = 1")
116
137
117 assert_equal false, Project.find(1).identifier_frozen?
138 assert_equal false, Project.find(1).identifier_frozen?
118 end
139 end
119
140
120 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
141 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
121 assert_equal true, Project.find(1).identifier_frozen?
142 assert_equal true, Project.find(1).identifier_frozen?
122 end
143 end
123
144
124 def test_members_should_be_active_users
145 def test_members_should_be_active_users
125 Project.all.each do |project|
146 Project.all.each do |project|
126 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
147 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
127 end
148 end
128 end
149 end
129
150
130 def test_users_should_be_active_users
151 def test_users_should_be_active_users
131 Project.all.each do |project|
152 Project.all.each do |project|
132 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
153 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
133 end
154 end
134 end
155 end
135
156
136 def test_open_scope_on_issues_association
157 def test_open_scope_on_issues_association
137 assert_kind_of Issue, Project.find(1).issues.open.first
158 assert_kind_of Issue, Project.find(1).issues.open.first
138 end
159 end
139
160
140 def test_archive
161 def test_archive
141 user = @ecookbook.members.first.user
162 user = @ecookbook.members.first.user
142 @ecookbook.archive
163 @ecookbook.archive
143 @ecookbook.reload
164 @ecookbook.reload
144
165
145 assert !@ecookbook.active?
166 assert !@ecookbook.active?
146 assert @ecookbook.archived?
167 assert @ecookbook.archived?
147 assert !user.projects.include?(@ecookbook)
168 assert !user.projects.include?(@ecookbook)
148 # Subproject are also archived
169 # Subproject are also archived
149 assert !@ecookbook.children.empty?
170 assert !@ecookbook.children.empty?
150 assert @ecookbook.descendants.active.empty?
171 assert @ecookbook.descendants.active.empty?
151 end
172 end
152
173
153 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
174 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
154 # Assign an issue of a project to a version of a child project
175 # Assign an issue of a project to a version of a child project
155 Issue.find(4).update_attribute :fixed_version_id, 4
176 Issue.find(4).update_attribute :fixed_version_id, 4
156
177
157 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
178 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
158 assert_equal false, @ecookbook.archive
179 assert_equal false, @ecookbook.archive
159 end
180 end
160 @ecookbook.reload
181 @ecookbook.reload
161 assert @ecookbook.active?
182 assert @ecookbook.active?
162 end
183 end
163
184
164 def test_unarchive
185 def test_unarchive
165 user = @ecookbook.members.first.user
186 user = @ecookbook.members.first.user
166 @ecookbook.archive
187 @ecookbook.archive
167 # A subproject of an archived project can not be unarchived
188 # A subproject of an archived project can not be unarchived
168 assert !@ecookbook_sub1.unarchive
189 assert !@ecookbook_sub1.unarchive
169
190
170 # Unarchive project
191 # Unarchive project
171 assert @ecookbook.unarchive
192 assert @ecookbook.unarchive
172 @ecookbook.reload
193 @ecookbook.reload
173 assert @ecookbook.active?
194 assert @ecookbook.active?
174 assert !@ecookbook.archived?
195 assert !@ecookbook.archived?
175 assert user.projects.include?(@ecookbook)
196 assert user.projects.include?(@ecookbook)
176 # Subproject can now be unarchived
197 # Subproject can now be unarchived
177 @ecookbook_sub1.reload
198 @ecookbook_sub1.reload
178 assert @ecookbook_sub1.unarchive
199 assert @ecookbook_sub1.unarchive
179 end
200 end
180
201
181 def test_destroy
202 def test_destroy
182 # 2 active members
203 # 2 active members
183 assert_equal 2, @ecookbook.members.size
204 assert_equal 2, @ecookbook.members.size
184 # and 1 is locked
205 # and 1 is locked
185 assert_equal 3, Member.where('project_id = ?', @ecookbook.id).all.size
206 assert_equal 3, Member.where('project_id = ?', @ecookbook.id).all.size
186 # some boards
207 # some boards
187 assert @ecookbook.boards.any?
208 assert @ecookbook.boards.any?
188
209
189 @ecookbook.destroy
210 @ecookbook.destroy
190 # make sure that the project non longer exists
211 # make sure that the project non longer exists
191 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
212 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
192 # make sure related data was removed
213 # make sure related data was removed
193 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
214 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
194 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
215 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
195 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
216 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
196 end
217 end
197
218
198 def test_destroy_should_destroy_subtasks
219 def test_destroy_should_destroy_subtasks
199 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
220 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
200 issues[0].update_attribute :parent_issue_id, issues[1].id
221 issues[0].update_attribute :parent_issue_id, issues[1].id
201 issues[2].update_attribute :parent_issue_id, issues[1].id
222 issues[2].update_attribute :parent_issue_id, issues[1].id
202 assert_equal 2, issues[1].children.count
223 assert_equal 2, issues[1].children.count
203
224
204 assert_nothing_raised do
225 assert_nothing_raised do
205 Project.find(1).destroy
226 Project.find(1).destroy
206 end
227 end
207 assert Issue.find_all_by_id(issues.map(&:id)).empty?
228 assert Issue.find_all_by_id(issues.map(&:id)).empty?
208 end
229 end
209
230
210 def test_destroying_root_projects_should_clear_data
231 def test_destroying_root_projects_should_clear_data
211 Project.roots.each do |root|
232 Project.roots.each do |root|
212 root.destroy
233 root.destroy
213 end
234 end
214
235
215 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
236 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
216 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
237 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
217 assert_equal 0, MemberRole.count
238 assert_equal 0, MemberRole.count
218 assert_equal 0, Issue.count
239 assert_equal 0, Issue.count
219 assert_equal 0, Journal.count
240 assert_equal 0, Journal.count
220 assert_equal 0, JournalDetail.count
241 assert_equal 0, JournalDetail.count
221 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
242 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
222 assert_equal 0, EnabledModule.count
243 assert_equal 0, EnabledModule.count
223 assert_equal 0, IssueCategory.count
244 assert_equal 0, IssueCategory.count
224 assert_equal 0, IssueRelation.count
245 assert_equal 0, IssueRelation.count
225 assert_equal 0, Board.count
246 assert_equal 0, Board.count
226 assert_equal 0, Message.count
247 assert_equal 0, Message.count
227 assert_equal 0, News.count
248 assert_equal 0, News.count
228 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
249 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
229 assert_equal 0, Repository.count
250 assert_equal 0, Repository.count
230 assert_equal 0, Changeset.count
251 assert_equal 0, Changeset.count
231 assert_equal 0, Change.count
252 assert_equal 0, Change.count
232 assert_equal 0, Comment.count
253 assert_equal 0, Comment.count
233 assert_equal 0, TimeEntry.count
254 assert_equal 0, TimeEntry.count
234 assert_equal 0, Version.count
255 assert_equal 0, Version.count
235 assert_equal 0, Watcher.count
256 assert_equal 0, Watcher.count
236 assert_equal 0, Wiki.count
257 assert_equal 0, Wiki.count
237 assert_equal 0, WikiPage.count
258 assert_equal 0, WikiPage.count
238 assert_equal 0, WikiContent.count
259 assert_equal 0, WikiContent.count
239 assert_equal 0, WikiContent::Version.count
260 assert_equal 0, WikiContent::Version.count
240 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
261 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
241 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
262 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
242 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
263 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
243 end
264 end
244
265
245 def test_move_an_orphan_project_to_a_root_project
266 def test_move_an_orphan_project_to_a_root_project
246 sub = Project.find(2)
267 sub = Project.find(2)
247 sub.set_parent! @ecookbook
268 sub.set_parent! @ecookbook
248 assert_equal @ecookbook.id, sub.parent.id
269 assert_equal @ecookbook.id, sub.parent.id
249 @ecookbook.reload
270 @ecookbook.reload
250 assert_equal 4, @ecookbook.children.size
271 assert_equal 4, @ecookbook.children.size
251 end
272 end
252
273
253 def test_move_an_orphan_project_to_a_subproject
274 def test_move_an_orphan_project_to_a_subproject
254 sub = Project.find(2)
275 sub = Project.find(2)
255 assert sub.set_parent!(@ecookbook_sub1)
276 assert sub.set_parent!(@ecookbook_sub1)
256 end
277 end
257
278
258 def test_move_a_root_project_to_a_project
279 def test_move_a_root_project_to_a_project
259 sub = @ecookbook
280 sub = @ecookbook
260 assert sub.set_parent!(Project.find(2))
281 assert sub.set_parent!(Project.find(2))
261 end
282 end
262
283
263 def test_should_not_move_a_project_to_its_children
284 def test_should_not_move_a_project_to_its_children
264 sub = @ecookbook
285 sub = @ecookbook
265 assert !(sub.set_parent!(Project.find(3)))
286 assert !(sub.set_parent!(Project.find(3)))
266 end
287 end
267
288
268 def test_set_parent_should_add_roots_in_alphabetical_order
289 def test_set_parent_should_add_roots_in_alphabetical_order
269 ProjectCustomField.delete_all
290 ProjectCustomField.delete_all
270 Project.delete_all
291 Project.delete_all
271 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
292 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
272 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
293 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
273 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
294 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
274 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
295 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
275
296
276 assert_equal 4, Project.count
297 assert_equal 4, Project.count
277 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
298 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
278 end
299 end
279
300
280 def test_set_parent_should_add_children_in_alphabetical_order
301 def test_set_parent_should_add_children_in_alphabetical_order
281 ProjectCustomField.delete_all
302 ProjectCustomField.delete_all
282 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
303 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
283 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
304 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
284 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
305 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
285 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
306 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
286 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
307 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
287
308
288 parent.reload
309 parent.reload
289 assert_equal 4, parent.children.size
310 assert_equal 4, parent.children.size
290 assert_equal parent.children.all.sort_by(&:name), parent.children.all
311 assert_equal parent.children.all.sort_by(&:name), parent.children.all
291 end
312 end
292
313
293 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
314 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
294 # Parent issue with a hierarchy project's fixed version
315 # Parent issue with a hierarchy project's fixed version
295 parent_issue = Issue.find(1)
316 parent_issue = Issue.find(1)
296 parent_issue.update_attribute(:fixed_version_id, 4)
317 parent_issue.update_attribute(:fixed_version_id, 4)
297 parent_issue.reload
318 parent_issue.reload
298 assert_equal 4, parent_issue.fixed_version_id
319 assert_equal 4, parent_issue.fixed_version_id
299
320
300 # Should keep fixed versions for the issues
321 # Should keep fixed versions for the issues
301 issue_with_local_fixed_version = Issue.find(5)
322 issue_with_local_fixed_version = Issue.find(5)
302 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
323 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
303 issue_with_local_fixed_version.reload
324 issue_with_local_fixed_version.reload
304 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
325 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
305
326
306 # Local issue with hierarchy fixed_version
327 # Local issue with hierarchy fixed_version
307 issue_with_hierarchy_fixed_version = Issue.find(13)
328 issue_with_hierarchy_fixed_version = Issue.find(13)
308 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
329 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
309 issue_with_hierarchy_fixed_version.reload
330 issue_with_hierarchy_fixed_version.reload
310 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
331 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
311
332
312 # Move project out of the issue's hierarchy
333 # Move project out of the issue's hierarchy
313 moved_project = Project.find(3)
334 moved_project = Project.find(3)
314 moved_project.set_parent!(Project.find(2))
335 moved_project.set_parent!(Project.find(2))
315 parent_issue.reload
336 parent_issue.reload
316 issue_with_local_fixed_version.reload
337 issue_with_local_fixed_version.reload
317 issue_with_hierarchy_fixed_version.reload
338 issue_with_hierarchy_fixed_version.reload
318
339
319 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
340 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
320 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"
341 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"
321 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
342 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
322 end
343 end
323
344
324 def test_parent
345 def test_parent
325 p = Project.find(6).parent
346 p = Project.find(6).parent
326 assert p.is_a?(Project)
347 assert p.is_a?(Project)
327 assert_equal 5, p.id
348 assert_equal 5, p.id
328 end
349 end
329
350
330 def test_ancestors
351 def test_ancestors
331 a = Project.find(6).ancestors
352 a = Project.find(6).ancestors
332 assert a.first.is_a?(Project)
353 assert a.first.is_a?(Project)
333 assert_equal [1, 5], a.collect(&:id)
354 assert_equal [1, 5], a.collect(&:id)
334 end
355 end
335
356
336 def test_root
357 def test_root
337 r = Project.find(6).root
358 r = Project.find(6).root
338 assert r.is_a?(Project)
359 assert r.is_a?(Project)
339 assert_equal 1, r.id
360 assert_equal 1, r.id
340 end
361 end
341
362
342 def test_children
363 def test_children
343 c = Project.find(1).children
364 c = Project.find(1).children
344 assert c.first.is_a?(Project)
365 assert c.first.is_a?(Project)
345 assert_equal [5, 3, 4], c.collect(&:id)
366 assert_equal [5, 3, 4], c.collect(&:id)
346 end
367 end
347
368
348 def test_descendants
369 def test_descendants
349 d = Project.find(1).descendants
370 d = Project.find(1).descendants
350 assert d.first.is_a?(Project)
371 assert d.first.is_a?(Project)
351 assert_equal [5, 6, 3, 4], d.collect(&:id)
372 assert_equal [5, 6, 3, 4], d.collect(&:id)
352 end
373 end
353
374
354 def test_allowed_parents_should_be_empty_for_non_member_user
375 def test_allowed_parents_should_be_empty_for_non_member_user
355 Role.non_member.add_permission!(:add_project)
376 Role.non_member.add_permission!(:add_project)
356 user = User.find(9)
377 user = User.find(9)
357 assert user.memberships.empty?
378 assert user.memberships.empty?
358 User.current = user
379 User.current = user
359 assert Project.new.allowed_parents.compact.empty?
380 assert Project.new.allowed_parents.compact.empty?
360 end
381 end
361
382
362 def test_allowed_parents_with_add_subprojects_permission
383 def test_allowed_parents_with_add_subprojects_permission
363 Role.find(1).remove_permission!(:add_project)
384 Role.find(1).remove_permission!(:add_project)
364 Role.find(1).add_permission!(:add_subprojects)
385 Role.find(1).add_permission!(:add_subprojects)
365 User.current = User.find(2)
386 User.current = User.find(2)
366 # new project
387 # new project
367 assert !Project.new.allowed_parents.include?(nil)
388 assert !Project.new.allowed_parents.include?(nil)
368 assert Project.new.allowed_parents.include?(Project.find(1))
389 assert Project.new.allowed_parents.include?(Project.find(1))
369 # existing root project
390 # existing root project
370 assert Project.find(1).allowed_parents.include?(nil)
391 assert Project.find(1).allowed_parents.include?(nil)
371 # existing child
392 # existing child
372 assert Project.find(3).allowed_parents.include?(Project.find(1))
393 assert Project.find(3).allowed_parents.include?(Project.find(1))
373 assert !Project.find(3).allowed_parents.include?(nil)
394 assert !Project.find(3).allowed_parents.include?(nil)
374 end
395 end
375
396
376 def test_allowed_parents_with_add_project_permission
397 def test_allowed_parents_with_add_project_permission
377 Role.find(1).add_permission!(:add_project)
398 Role.find(1).add_permission!(:add_project)
378 Role.find(1).remove_permission!(:add_subprojects)
399 Role.find(1).remove_permission!(:add_subprojects)
379 User.current = User.find(2)
400 User.current = User.find(2)
380 # new project
401 # new project
381 assert Project.new.allowed_parents.include?(nil)
402 assert Project.new.allowed_parents.include?(nil)
382 assert !Project.new.allowed_parents.include?(Project.find(1))
403 assert !Project.new.allowed_parents.include?(Project.find(1))
383 # existing root project
404 # existing root project
384 assert Project.find(1).allowed_parents.include?(nil)
405 assert Project.find(1).allowed_parents.include?(nil)
385 # existing child
406 # existing child
386 assert Project.find(3).allowed_parents.include?(Project.find(1))
407 assert Project.find(3).allowed_parents.include?(Project.find(1))
387 assert Project.find(3).allowed_parents.include?(nil)
408 assert Project.find(3).allowed_parents.include?(nil)
388 end
409 end
389
410
390 def test_allowed_parents_with_add_project_and_subprojects_permission
411 def test_allowed_parents_with_add_project_and_subprojects_permission
391 Role.find(1).add_permission!(:add_project)
412 Role.find(1).add_permission!(:add_project)
392 Role.find(1).add_permission!(:add_subprojects)
413 Role.find(1).add_permission!(:add_subprojects)
393 User.current = User.find(2)
414 User.current = User.find(2)
394 # new project
415 # new project
395 assert Project.new.allowed_parents.include?(nil)
416 assert Project.new.allowed_parents.include?(nil)
396 assert Project.new.allowed_parents.include?(Project.find(1))
417 assert Project.new.allowed_parents.include?(Project.find(1))
397 # existing root project
418 # existing root project
398 assert Project.find(1).allowed_parents.include?(nil)
419 assert Project.find(1).allowed_parents.include?(nil)
399 # existing child
420 # existing child
400 assert Project.find(3).allowed_parents.include?(Project.find(1))
421 assert Project.find(3).allowed_parents.include?(Project.find(1))
401 assert Project.find(3).allowed_parents.include?(nil)
422 assert Project.find(3).allowed_parents.include?(nil)
402 end
423 end
403
424
404 def test_users_by_role
425 def test_users_by_role
405 users_by_role = Project.find(1).users_by_role
426 users_by_role = Project.find(1).users_by_role
406 assert_kind_of Hash, users_by_role
427 assert_kind_of Hash, users_by_role
407 role = Role.find(1)
428 role = Role.find(1)
408 assert_kind_of Array, users_by_role[role]
429 assert_kind_of Array, users_by_role[role]
409 assert users_by_role[role].include?(User.find(2))
430 assert users_by_role[role].include?(User.find(2))
410 end
431 end
411
432
412 def test_rolled_up_trackers
433 def test_rolled_up_trackers
413 parent = Project.find(1)
434 parent = Project.find(1)
414 parent.trackers = Tracker.find([1,2])
435 parent.trackers = Tracker.find([1,2])
415 child = parent.children.find(3)
436 child = parent.children.find(3)
416
437
417 assert_equal [1, 2], parent.tracker_ids
438 assert_equal [1, 2], parent.tracker_ids
418 assert_equal [2, 3], child.trackers.collect(&:id)
439 assert_equal [2, 3], child.trackers.collect(&:id)
419
440
420 assert_kind_of Tracker, parent.rolled_up_trackers.first
441 assert_kind_of Tracker, parent.rolled_up_trackers.first
421 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
442 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
422
443
423 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
444 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
424 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
445 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
425 end
446 end
426
447
427 def test_rolled_up_trackers_should_ignore_archived_subprojects
448 def test_rolled_up_trackers_should_ignore_archived_subprojects
428 parent = Project.find(1)
449 parent = Project.find(1)
429 parent.trackers = Tracker.find([1,2])
450 parent.trackers = Tracker.find([1,2])
430 child = parent.children.find(3)
451 child = parent.children.find(3)
431 child.trackers = Tracker.find([1,3])
452 child.trackers = Tracker.find([1,3])
432 parent.children.each(&:archive)
453 parent.children.each(&:archive)
433
454
434 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
455 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
435 end
456 end
436
457
437 test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do
458 test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do
438 parent = Project.generate!
459 parent = Project.generate!
439 parent.trackers = Tracker.find([1, 2])
460 parent.trackers = Tracker.find([1, 2])
440 child = Project.generate_with_parent!(parent)
461 child = Project.generate_with_parent!(parent)
441 child.trackers = Tracker.find([2, 3])
462 child.trackers = Tracker.find([2, 3])
442
463
443 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort
464 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort
444
465
445 assert child.disable_module!(:issue_tracking)
466 assert child.disable_module!(:issue_tracking)
446 parent.reload
467 parent.reload
447 assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort
468 assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort
448 end
469 end
449
470
450 test "#rolled_up_versions should include the versions for the current project" do
471 test "#rolled_up_versions should include the versions for the current project" do
451 project = Project.generate!
472 project = Project.generate!
452 parent_version_1 = Version.generate!(:project => project)
473 parent_version_1 = Version.generate!(:project => project)
453 parent_version_2 = Version.generate!(:project => project)
474 parent_version_2 = Version.generate!(:project => project)
454 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
475 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
455 end
476 end
456
477
457 test "#rolled_up_versions should include versions for a subproject" do
478 test "#rolled_up_versions should include versions for a subproject" do
458 project = Project.generate!
479 project = Project.generate!
459 parent_version_1 = Version.generate!(:project => project)
480 parent_version_1 = Version.generate!(:project => project)
460 parent_version_2 = Version.generate!(:project => project)
481 parent_version_2 = Version.generate!(:project => project)
461 subproject = Project.generate_with_parent!(project)
482 subproject = Project.generate_with_parent!(project)
462 subproject_version = Version.generate!(:project => subproject)
483 subproject_version = Version.generate!(:project => subproject)
463
484
464 assert_same_elements [
485 assert_same_elements [
465 parent_version_1,
486 parent_version_1,
466 parent_version_2,
487 parent_version_2,
467 subproject_version
488 subproject_version
468 ], project.rolled_up_versions
489 ], project.rolled_up_versions
469 end
490 end
470
491
471 test "#rolled_up_versions should include versions for a sub-subproject" do
492 test "#rolled_up_versions should include versions for a sub-subproject" do
472 project = Project.generate!
493 project = Project.generate!
473 parent_version_1 = Version.generate!(:project => project)
494 parent_version_1 = Version.generate!(:project => project)
474 parent_version_2 = Version.generate!(:project => project)
495 parent_version_2 = Version.generate!(:project => project)
475 subproject = Project.generate_with_parent!(project)
496 subproject = Project.generate_with_parent!(project)
476 sub_subproject = Project.generate_with_parent!(subproject)
497 sub_subproject = Project.generate_with_parent!(subproject)
477 sub_subproject_version = Version.generate!(:project => sub_subproject)
498 sub_subproject_version = Version.generate!(:project => sub_subproject)
478 project.reload
499 project.reload
479
500
480 assert_same_elements [
501 assert_same_elements [
481 parent_version_1,
502 parent_version_1,
482 parent_version_2,
503 parent_version_2,
483 sub_subproject_version
504 sub_subproject_version
484 ], project.rolled_up_versions
505 ], project.rolled_up_versions
485 end
506 end
486
507
487 test "#rolled_up_versions should only check active projects" do
508 test "#rolled_up_versions should only check active projects" do
488 project = Project.generate!
509 project = Project.generate!
489 parent_version_1 = Version.generate!(:project => project)
510 parent_version_1 = Version.generate!(:project => project)
490 parent_version_2 = Version.generate!(:project => project)
511 parent_version_2 = Version.generate!(:project => project)
491 subproject = Project.generate_with_parent!(project)
512 subproject = Project.generate_with_parent!(project)
492 subproject_version = Version.generate!(:project => subproject)
513 subproject_version = Version.generate!(:project => subproject)
493 assert subproject.archive
514 assert subproject.archive
494 project.reload
515 project.reload
495
516
496 assert !subproject.active?
517 assert !subproject.active?
497 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
518 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
498 end
519 end
499
520
500 def test_shared_versions_none_sharing
521 def test_shared_versions_none_sharing
501 p = Project.find(5)
522 p = Project.find(5)
502 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
523 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
503 assert p.shared_versions.include?(v)
524 assert p.shared_versions.include?(v)
504 assert !p.children.first.shared_versions.include?(v)
525 assert !p.children.first.shared_versions.include?(v)
505 assert !p.root.shared_versions.include?(v)
526 assert !p.root.shared_versions.include?(v)
506 assert !p.siblings.first.shared_versions.include?(v)
527 assert !p.siblings.first.shared_versions.include?(v)
507 assert !p.root.siblings.first.shared_versions.include?(v)
528 assert !p.root.siblings.first.shared_versions.include?(v)
508 end
529 end
509
530
510 def test_shared_versions_descendants_sharing
531 def test_shared_versions_descendants_sharing
511 p = Project.find(5)
532 p = Project.find(5)
512 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
533 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
513 assert p.shared_versions.include?(v)
534 assert p.shared_versions.include?(v)
514 assert p.children.first.shared_versions.include?(v)
535 assert p.children.first.shared_versions.include?(v)
515 assert !p.root.shared_versions.include?(v)
536 assert !p.root.shared_versions.include?(v)
516 assert !p.siblings.first.shared_versions.include?(v)
537 assert !p.siblings.first.shared_versions.include?(v)
517 assert !p.root.siblings.first.shared_versions.include?(v)
538 assert !p.root.siblings.first.shared_versions.include?(v)
518 end
539 end
519
540
520 def test_shared_versions_hierarchy_sharing
541 def test_shared_versions_hierarchy_sharing
521 p = Project.find(5)
542 p = Project.find(5)
522 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
543 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
523 assert p.shared_versions.include?(v)
544 assert p.shared_versions.include?(v)
524 assert p.children.first.shared_versions.include?(v)
545 assert p.children.first.shared_versions.include?(v)
525 assert p.root.shared_versions.include?(v)
546 assert p.root.shared_versions.include?(v)
526 assert !p.siblings.first.shared_versions.include?(v)
547 assert !p.siblings.first.shared_versions.include?(v)
527 assert !p.root.siblings.first.shared_versions.include?(v)
548 assert !p.root.siblings.first.shared_versions.include?(v)
528 end
549 end
529
550
530 def test_shared_versions_tree_sharing
551 def test_shared_versions_tree_sharing
531 p = Project.find(5)
552 p = Project.find(5)
532 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
553 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
533 assert p.shared_versions.include?(v)
554 assert p.shared_versions.include?(v)
534 assert p.children.first.shared_versions.include?(v)
555 assert p.children.first.shared_versions.include?(v)
535 assert p.root.shared_versions.include?(v)
556 assert p.root.shared_versions.include?(v)
536 assert p.siblings.first.shared_versions.include?(v)
557 assert p.siblings.first.shared_versions.include?(v)
537 assert !p.root.siblings.first.shared_versions.include?(v)
558 assert !p.root.siblings.first.shared_versions.include?(v)
538 end
559 end
539
560
540 def test_shared_versions_system_sharing
561 def test_shared_versions_system_sharing
541 p = Project.find(5)
562 p = Project.find(5)
542 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
563 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
543 assert p.shared_versions.include?(v)
564 assert p.shared_versions.include?(v)
544 assert p.children.first.shared_versions.include?(v)
565 assert p.children.first.shared_versions.include?(v)
545 assert p.root.shared_versions.include?(v)
566 assert p.root.shared_versions.include?(v)
546 assert p.siblings.first.shared_versions.include?(v)
567 assert p.siblings.first.shared_versions.include?(v)
547 assert p.root.siblings.first.shared_versions.include?(v)
568 assert p.root.siblings.first.shared_versions.include?(v)
548 end
569 end
549
570
550 def test_shared_versions
571 def test_shared_versions
551 parent = Project.find(1)
572 parent = Project.find(1)
552 child = parent.children.find(3)
573 child = parent.children.find(3)
553 private_child = parent.children.find(5)
574 private_child = parent.children.find(5)
554
575
555 assert_equal [1,2,3], parent.version_ids.sort
576 assert_equal [1,2,3], parent.version_ids.sort
556 assert_equal [4], child.version_ids
577 assert_equal [4], child.version_ids
557 assert_equal [6], private_child.version_ids
578 assert_equal [6], private_child.version_ids
558 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
579 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
559
580
560 assert_equal 6, parent.shared_versions.size
581 assert_equal 6, parent.shared_versions.size
561 parent.shared_versions.each do |version|
582 parent.shared_versions.each do |version|
562 assert_kind_of Version, version
583 assert_kind_of Version, version
563 end
584 end
564
585
565 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
586 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
566 end
587 end
567
588
568 def test_shared_versions_should_ignore_archived_subprojects
589 def test_shared_versions_should_ignore_archived_subprojects
569 parent = Project.find(1)
590 parent = Project.find(1)
570 child = parent.children.find(3)
591 child = parent.children.find(3)
571 child.archive
592 child.archive
572 parent.reload
593 parent.reload
573
594
574 assert_equal [1,2,3], parent.version_ids.sort
595 assert_equal [1,2,3], parent.version_ids.sort
575 assert_equal [4], child.version_ids
596 assert_equal [4], child.version_ids
576 assert !parent.shared_versions.collect(&:id).include?(4)
597 assert !parent.shared_versions.collect(&:id).include?(4)
577 end
598 end
578
599
579 def test_shared_versions_visible_to_user
600 def test_shared_versions_visible_to_user
580 user = User.find(3)
601 user = User.find(3)
581 parent = Project.find(1)
602 parent = Project.find(1)
582 child = parent.children.find(5)
603 child = parent.children.find(5)
583
604
584 assert_equal [1,2,3], parent.version_ids.sort
605 assert_equal [1,2,3], parent.version_ids.sort
585 assert_equal [6], child.version_ids
606 assert_equal [6], child.version_ids
586
607
587 versions = parent.shared_versions.visible(user)
608 versions = parent.shared_versions.visible(user)
588
609
589 assert_equal 4, versions.size
610 assert_equal 4, versions.size
590 versions.each do |version|
611 versions.each do |version|
591 assert_kind_of Version, version
612 assert_kind_of Version, version
592 end
613 end
593
614
594 assert !versions.collect(&:id).include?(6)
615 assert !versions.collect(&:id).include?(6)
595 end
616 end
596
617
597 def test_shared_versions_for_new_project_should_include_system_shared_versions
618 def test_shared_versions_for_new_project_should_include_system_shared_versions
598 p = Project.find(5)
619 p = Project.find(5)
599 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
620 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
600
621
601 assert_include v, Project.new.shared_versions
622 assert_include v, Project.new.shared_versions
602 end
623 end
603
624
604 def test_next_identifier
625 def test_next_identifier
605 ProjectCustomField.delete_all
626 ProjectCustomField.delete_all
606 Project.create!(:name => 'last', :identifier => 'p2008040')
627 Project.create!(:name => 'last', :identifier => 'p2008040')
607 assert_equal 'p2008041', Project.next_identifier
628 assert_equal 'p2008041', Project.next_identifier
608 end
629 end
609
630
610 def test_next_identifier_first_project
631 def test_next_identifier_first_project
611 Project.delete_all
632 Project.delete_all
612 assert_nil Project.next_identifier
633 assert_nil Project.next_identifier
613 end
634 end
614
635
615 def test_enabled_module_names
636 def test_enabled_module_names
616 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
637 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
617 project = Project.new
638 project = Project.new
618
639
619 project.enabled_module_names = %w(issue_tracking news)
640 project.enabled_module_names = %w(issue_tracking news)
620 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
641 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
621 end
642 end
622 end
643 end
623
644
624 test "enabled_modules should define module by names and preserve ids" do
645 test "enabled_modules should define module by names and preserve ids" do
625 @project = Project.find(1)
646 @project = Project.find(1)
626 # Remove one module
647 # Remove one module
627 modules = @project.enabled_modules.slice(0..-2)
648 modules = @project.enabled_modules.slice(0..-2)
628 assert modules.any?
649 assert modules.any?
629 assert_difference 'EnabledModule.count', -1 do
650 assert_difference 'EnabledModule.count', -1 do
630 @project.enabled_module_names = modules.collect(&:name)
651 @project.enabled_module_names = modules.collect(&:name)
631 end
652 end
632 @project.reload
653 @project.reload
633 # Ids should be preserved
654 # Ids should be preserved
634 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
655 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
635 end
656 end
636
657
637 test "enabled_modules should enable a module" do
658 test "enabled_modules should enable a module" do
638 @project = Project.find(1)
659 @project = Project.find(1)
639 @project.enabled_module_names = []
660 @project.enabled_module_names = []
640 @project.reload
661 @project.reload
641 assert_equal [], @project.enabled_module_names
662 assert_equal [], @project.enabled_module_names
642 #with string
663 #with string
643 @project.enable_module!("issue_tracking")
664 @project.enable_module!("issue_tracking")
644 assert_equal ["issue_tracking"], @project.enabled_module_names
665 assert_equal ["issue_tracking"], @project.enabled_module_names
645 #with symbol
666 #with symbol
646 @project.enable_module!(:gantt)
667 @project.enable_module!(:gantt)
647 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
668 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
648 #don't add a module twice
669 #don't add a module twice
649 @project.enable_module!("issue_tracking")
670 @project.enable_module!("issue_tracking")
650 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
671 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
651 end
672 end
652
673
653 test "enabled_modules should disable a module" do
674 test "enabled_modules should disable a module" do
654 @project = Project.find(1)
675 @project = Project.find(1)
655 #with string
676 #with string
656 assert @project.enabled_module_names.include?("issue_tracking")
677 assert @project.enabled_module_names.include?("issue_tracking")
657 @project.disable_module!("issue_tracking")
678 @project.disable_module!("issue_tracking")
658 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
679 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
659 #with symbol
680 #with symbol
660 assert @project.enabled_module_names.include?("gantt")
681 assert @project.enabled_module_names.include?("gantt")
661 @project.disable_module!(:gantt)
682 @project.disable_module!(:gantt)
662 assert ! @project.reload.enabled_module_names.include?("gantt")
683 assert ! @project.reload.enabled_module_names.include?("gantt")
663 #with EnabledModule object
684 #with EnabledModule object
664 first_module = @project.enabled_modules.first
685 first_module = @project.enabled_modules.first
665 @project.disable_module!(first_module)
686 @project.disable_module!(first_module)
666 assert ! @project.reload.enabled_module_names.include?(first_module.name)
687 assert ! @project.reload.enabled_module_names.include?(first_module.name)
667 end
688 end
668
689
669 def test_enabled_module_names_should_not_recreate_enabled_modules
690 def test_enabled_module_names_should_not_recreate_enabled_modules
670 project = Project.find(1)
691 project = Project.find(1)
671 # Remove one module
692 # Remove one module
672 modules = project.enabled_modules.slice(0..-2)
693 modules = project.enabled_modules.slice(0..-2)
673 assert modules.any?
694 assert modules.any?
674 assert_difference 'EnabledModule.count', -1 do
695 assert_difference 'EnabledModule.count', -1 do
675 project.enabled_module_names = modules.collect(&:name)
696 project.enabled_module_names = modules.collect(&:name)
676 end
697 end
677 project.reload
698 project.reload
678 # Ids should be preserved
699 # Ids should be preserved
679 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
700 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
680 end
701 end
681
702
682 def test_copy_from_existing_project
703 def test_copy_from_existing_project
683 source_project = Project.find(1)
704 source_project = Project.find(1)
684 copied_project = Project.copy_from(1)
705 copied_project = Project.copy_from(1)
685
706
686 assert copied_project
707 assert copied_project
687 # Cleared attributes
708 # Cleared attributes
688 assert copied_project.id.blank?
709 assert copied_project.id.blank?
689 assert copied_project.name.blank?
710 assert copied_project.name.blank?
690 assert copied_project.identifier.blank?
711 assert copied_project.identifier.blank?
691
712
692 # Duplicated attributes
713 # Duplicated attributes
693 assert_equal source_project.description, copied_project.description
714 assert_equal source_project.description, copied_project.description
694 assert_equal source_project.enabled_modules, copied_project.enabled_modules
715 assert_equal source_project.enabled_modules, copied_project.enabled_modules
695 assert_equal source_project.trackers, copied_project.trackers
716 assert_equal source_project.trackers, copied_project.trackers
696
717
697 # Default attributes
718 # Default attributes
698 assert_equal 1, copied_project.status
719 assert_equal 1, copied_project.status
699 end
720 end
700
721
701 def test_activities_should_use_the_system_activities
722 def test_activities_should_use_the_system_activities
702 project = Project.find(1)
723 project = Project.find(1)
703 assert_equal project.activities, TimeEntryActivity.where(:active => true).all
724 assert_equal project.activities, TimeEntryActivity.where(:active => true).all
704 end
725 end
705
726
706
727
707 def test_activities_should_use_the_project_specific_activities
728 def test_activities_should_use_the_project_specific_activities
708 project = Project.find(1)
729 project = Project.find(1)
709 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
730 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
710 assert overridden_activity.save!
731 assert overridden_activity.save!
711
732
712 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
733 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
713 end
734 end
714
735
715 def test_activities_should_not_include_the_inactive_project_specific_activities
736 def test_activities_should_not_include_the_inactive_project_specific_activities
716 project = Project.find(1)
737 project = Project.find(1)
717 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
738 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
718 assert overridden_activity.save!
739 assert overridden_activity.save!
719
740
720 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
741 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
721 end
742 end
722
743
723 def test_activities_should_not_include_project_specific_activities_from_other_projects
744 def test_activities_should_not_include_project_specific_activities_from_other_projects
724 project = Project.find(1)
745 project = Project.find(1)
725 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
746 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
726 assert overridden_activity.save!
747 assert overridden_activity.save!
727
748
728 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
749 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
729 end
750 end
730
751
731 def test_activities_should_handle_nils
752 def test_activities_should_handle_nils
732 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first})
753 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first})
733 TimeEntryActivity.delete_all
754 TimeEntryActivity.delete_all
734
755
735 # No activities
756 # No activities
736 project = Project.find(1)
757 project = Project.find(1)
737 assert project.activities.empty?
758 assert project.activities.empty?
738
759
739 # No system, one overridden
760 # No system, one overridden
740 assert overridden_activity.save!
761 assert overridden_activity.save!
741 project.reload
762 project.reload
742 assert_equal [overridden_activity], project.activities
763 assert_equal [overridden_activity], project.activities
743 end
764 end
744
765
745 def test_activities_should_override_system_activities_with_project_activities
766 def test_activities_should_override_system_activities_with_project_activities
746 project = Project.find(1)
767 project = Project.find(1)
747 parent_activity = TimeEntryActivity.first
768 parent_activity = TimeEntryActivity.first
748 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
769 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
749 assert overridden_activity.save!
770 assert overridden_activity.save!
750
771
751 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
772 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
752 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
773 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
753 end
774 end
754
775
755 def test_activities_should_include_inactive_activities_if_specified
776 def test_activities_should_include_inactive_activities_if_specified
756 project = Project.find(1)
777 project = Project.find(1)
757 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
778 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
758 assert overridden_activity.save!
779 assert overridden_activity.save!
759
780
760 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
781 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
761 end
782 end
762
783
763 test 'activities should not include active System activities if the project has an override that is inactive' do
784 test 'activities should not include active System activities if the project has an override that is inactive' do
764 project = Project.find(1)
785 project = Project.find(1)
765 system_activity = TimeEntryActivity.find_by_name('Design')
786 system_activity = TimeEntryActivity.find_by_name('Design')
766 assert system_activity.active?
787 assert system_activity.active?
767 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
788 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
768 assert overridden_activity.save!
789 assert overridden_activity.save!
769
790
770 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
791 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
771 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
792 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
772 end
793 end
773
794
774 def test_close_completed_versions
795 def test_close_completed_versions
775 Version.update_all("status = 'open'")
796 Version.update_all("status = 'open'")
776 project = Project.find(1)
797 project = Project.find(1)
777 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
798 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
778 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
799 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
779 project.close_completed_versions
800 project.close_completed_versions
780 project.reload
801 project.reload
781 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
802 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
782 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
803 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
783 end
804 end
784
805
785 test "#start_date should be nil if there are no issues on the project" do
806 test "#start_date should be nil if there are no issues on the project" do
786 project = Project.generate!
807 project = Project.generate!
787 assert_nil project.start_date
808 assert_nil project.start_date
788 end
809 end
789
810
790 test "#start_date should be nil when issues have no start date" do
811 test "#start_date should be nil when issues have no start date" do
791 project = Project.generate!
812 project = Project.generate!
792 project.trackers << Tracker.generate!
813 project.trackers << Tracker.generate!
793 early = 7.days.ago.to_date
814 early = 7.days.ago.to_date
794 Issue.generate!(:project => project, :start_date => nil)
815 Issue.generate!(:project => project, :start_date => nil)
795
816
796 assert_nil project.start_date
817 assert_nil project.start_date
797 end
818 end
798
819
799 test "#start_date should be the earliest start date of it's issues" do
820 test "#start_date should be the earliest start date of it's issues" do
800 project = Project.generate!
821 project = Project.generate!
801 project.trackers << Tracker.generate!
822 project.trackers << Tracker.generate!
802 early = 7.days.ago.to_date
823 early = 7.days.ago.to_date
803 Issue.generate!(:project => project, :start_date => Date.today)
824 Issue.generate!(:project => project, :start_date => Date.today)
804 Issue.generate!(:project => project, :start_date => early)
825 Issue.generate!(:project => project, :start_date => early)
805
826
806 assert_equal early, project.start_date
827 assert_equal early, project.start_date
807 end
828 end
808
829
809 test "#due_date should be nil if there are no issues on the project" do
830 test "#due_date should be nil if there are no issues on the project" do
810 project = Project.generate!
831 project = Project.generate!
811 assert_nil project.due_date
832 assert_nil project.due_date
812 end
833 end
813
834
814 test "#due_date should be nil if there are no issues with due dates" do
835 test "#due_date should be nil if there are no issues with due dates" do
815 project = Project.generate!
836 project = Project.generate!
816 project.trackers << Tracker.generate!
837 project.trackers << Tracker.generate!
817 Issue.generate!(:project => project, :due_date => nil)
838 Issue.generate!(:project => project, :due_date => nil)
818
839
819 assert_nil project.due_date
840 assert_nil project.due_date
820 end
841 end
821
842
822 test "#due_date should be the latest due date of it's issues" do
843 test "#due_date should be the latest due date of it's issues" do
823 project = Project.generate!
844 project = Project.generate!
824 project.trackers << Tracker.generate!
845 project.trackers << Tracker.generate!
825 future = 7.days.from_now.to_date
846 future = 7.days.from_now.to_date
826 Issue.generate!(:project => project, :due_date => future)
847 Issue.generate!(:project => project, :due_date => future)
827 Issue.generate!(:project => project, :due_date => Date.today)
848 Issue.generate!(:project => project, :due_date => Date.today)
828
849
829 assert_equal future, project.due_date
850 assert_equal future, project.due_date
830 end
851 end
831
852
832 test "#due_date should be the latest due date of it's versions" do
853 test "#due_date should be the latest due date of it's versions" do
833 project = Project.generate!
854 project = Project.generate!
834 future = 7.days.from_now.to_date
855 future = 7.days.from_now.to_date
835 project.versions << Version.generate!(:effective_date => future)
856 project.versions << Version.generate!(:effective_date => future)
836 project.versions << Version.generate!(:effective_date => Date.today)
857 project.versions << Version.generate!(:effective_date => Date.today)
837
858
838 assert_equal future, project.due_date
859 assert_equal future, project.due_date
839 end
860 end
840
861
841 test "#due_date should pick the latest date from it's issues and versions" do
862 test "#due_date should pick the latest date from it's issues and versions" do
842 project = Project.generate!
863 project = Project.generate!
843 project.trackers << Tracker.generate!
864 project.trackers << Tracker.generate!
844 future = 7.days.from_now.to_date
865 future = 7.days.from_now.to_date
845 far_future = 14.days.from_now.to_date
866 far_future = 14.days.from_now.to_date
846 Issue.generate!(:project => project, :due_date => far_future)
867 Issue.generate!(:project => project, :due_date => far_future)
847 project.versions << Version.generate!(:effective_date => future)
868 project.versions << Version.generate!(:effective_date => future)
848
869
849 assert_equal far_future, project.due_date
870 assert_equal far_future, project.due_date
850 end
871 end
851
872
852 test "#completed_percent with no versions should be 100" do
873 test "#completed_percent with no versions should be 100" do
853 project = Project.generate!
874 project = Project.generate!
854 assert_equal 100, project.completed_percent
875 assert_equal 100, project.completed_percent
855 end
876 end
856
877
857 test "#completed_percent with versions should return 0 if the versions have no issues" do
878 test "#completed_percent with versions should return 0 if the versions have no issues" do
858 project = Project.generate!
879 project = Project.generate!
859 Version.generate!(:project => project)
880 Version.generate!(:project => project)
860 Version.generate!(:project => project)
881 Version.generate!(:project => project)
861
882
862 assert_equal 0, project.completed_percent
883 assert_equal 0, project.completed_percent
863 end
884 end
864
885
865 test "#completed_percent with versions should return 100 if the version has only closed issues" do
886 test "#completed_percent with versions should return 100 if the version has only closed issues" do
866 project = Project.generate!
887 project = Project.generate!
867 project.trackers << Tracker.generate!
888 project.trackers << Tracker.generate!
868 v1 = Version.generate!(:project => project)
889 v1 = Version.generate!(:project => project)
869 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
890 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
870 v2 = Version.generate!(:project => project)
891 v2 = Version.generate!(:project => project)
871 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
892 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
872
893
873 assert_equal 100, project.completed_percent
894 assert_equal 100, project.completed_percent
874 end
895 end
875
896
876 test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do
897 test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do
877 project = Project.generate!
898 project = Project.generate!
878 project.trackers << Tracker.generate!
899 project.trackers << Tracker.generate!
879 v1 = Version.generate!(:project => project)
900 v1 = Version.generate!(:project => project)
880 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
901 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
881 v2 = Version.generate!(:project => project)
902 v2 = Version.generate!(:project => project)
882 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
903 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
883
904
884 assert_equal 50, project.completed_percent
905 assert_equal 50, project.completed_percent
885 end
906 end
886
907
887 test "#notified_users" do
908 test "#notified_users" do
888 project = Project.generate!
909 project = Project.generate!
889 role = Role.generate!
910 role = Role.generate!
890
911
891 user_with_membership_notification = User.generate!(:mail_notification => 'selected')
912 user_with_membership_notification = User.generate!(:mail_notification => 'selected')
892 Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true)
913 Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true)
893
914
894 all_events_user = User.generate!(:mail_notification => 'all')
915 all_events_user = User.generate!(:mail_notification => 'all')
895 Member.create!(:project => project, :roles => [role], :principal => all_events_user)
916 Member.create!(:project => project, :roles => [role], :principal => all_events_user)
896
917
897 no_events_user = User.generate!(:mail_notification => 'none')
918 no_events_user = User.generate!(:mail_notification => 'none')
898 Member.create!(:project => project, :roles => [role], :principal => no_events_user)
919 Member.create!(:project => project, :roles => [role], :principal => no_events_user)
899
920
900 only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
921 only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
901 Member.create!(:project => project, :roles => [role], :principal => only_my_events_user)
922 Member.create!(:project => project, :roles => [role], :principal => only_my_events_user)
902
923
903 only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
924 only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
904 Member.create!(:project => project, :roles => [role], :principal => only_assigned_user)
925 Member.create!(:project => project, :roles => [role], :principal => only_assigned_user)
905
926
906 only_owned_user = User.generate!(:mail_notification => 'only_owner')
927 only_owned_user = User.generate!(:mail_notification => 'only_owner')
907 Member.create!(:project => project, :roles => [role], :principal => only_owned_user)
928 Member.create!(:project => project, :roles => [role], :principal => only_owned_user)
908
929
909 assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification"
930 assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification"
910 assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option"
931 assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option"
911 assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option"
932 assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option"
912 assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option"
933 assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option"
913 assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option"
934 assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option"
914 assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option"
935 assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option"
915 end
936 end
916 end
937 end
General Comments 0
You need to be logged in to leave comments. Login now