##// END OF EJS Templates
Changed the Timelogs to use both the Systemwide and Project specific TimeEntryActivities...
Eric Davis -
r2834:e615266e9a03
parent child
Show More
@@ -0,0 +1,56
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../../test_helper'
19
20 class TimelogHelperTest < HelperTestCase
21 include TimelogHelper
22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::DateHelper
24
25 fixtures :projects, :roles, :enabled_modules, :users,
26 :repositories, :changesets,
27 :trackers, :issue_statuses, :issues, :versions, :documents,
28 :wikis, :wiki_pages, :wiki_contents,
29 :boards, :messages,
30 :attachments,
31 :enumerations
32
33 def setup
34 super
35 end
36
37 def test_activities_collection_for_select_options_should_return_array_of_activity_names_and_ids
38 activities = activity_collection_for_select_options
39 assert activities.include?(["Design", 9])
40 assert activities.include?(["Development", 10])
41 end
42
43 def test_activities_collection_for_select_options_should_not_include_inactive_activities
44 activities = activity_collection_for_select_options
45 assert !activities.include?(["Inactive Activity", 14])
46 end
47
48 def test_activities_collection_for_select_options_should_use_the_projects_override
49 project = Project.find(1)
50 override_activity = TimeEntryActivity.create!({:name => "Design override", :parent => TimeEntryActivity.find_by_name("Design"), :project => project})
51
52 activities = activity_collection_for_select_options(nil, project)
53 assert !activities.include?(["Design", 9]), "System activity found in: " + activities.inspect
54 assert activities.include?(["Design override", override_activity.id]), "Override activity not found in: " + activities.inspect
55 end
56 end
@@ -1,171 +1,177
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module TimelogHelper
18 module TimelogHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def render_timelog_breadcrumb
21 def render_timelog_breadcrumb
22 links = []
22 links = []
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 links << link_to_issue(@issue) if @issue
25 links << link_to_issue(@issue) if @issue
26 breadcrumb links
26 breadcrumb links
27 end
27 end
28
28
29 # Returns a collection of activities for a select field. time_entry
29 # Returns a collection of activities for a select field. time_entry
30 # is optional and will be used to check if the selected TimeEntryActivity
30 # is optional and will be used to check if the selected TimeEntryActivity
31 # is active.
31 # is active.
32 def activity_collection_for_select_options(time_entry=nil)
32 def activity_collection_for_select_options(time_entry=nil, project=nil)
33 project ||= @project
34 if project.nil?
33 activities = TimeEntryActivity.active
35 activities = TimeEntryActivity.active
36 else
37 activities = project.activities
38 end
39
34 collection = []
40 collection = []
35 if time_entry && !time_entry.activity.active?
41 if time_entry && time_entry.activity && !time_entry.activity.active?
36 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
42 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
37 else
43 else
38 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
44 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
39 end
45 end
40 activities.each { |a| collection << [a.name, a.id] }
46 activities.each { |a| collection << [a.name, a.id] }
41 collection
47 collection
42 end
48 end
43
49
44 def select_hours(data, criteria, value)
50 def select_hours(data, criteria, value)
45 if value.to_s.empty?
51 if value.to_s.empty?
46 data.select {|row| row[criteria].blank? }
52 data.select {|row| row[criteria].blank? }
47 else
53 else
48 data.select {|row| row[criteria] == value}
54 data.select {|row| row[criteria] == value}
49 end
55 end
50 end
56 end
51
57
52 def sum_hours(data)
58 def sum_hours(data)
53 sum = 0
59 sum = 0
54 data.each do |row|
60 data.each do |row|
55 sum += row['hours'].to_f
61 sum += row['hours'].to_f
56 end
62 end
57 sum
63 sum
58 end
64 end
59
65
60 def options_for_period_select(value)
66 def options_for_period_select(value)
61 options_for_select([[l(:label_all_time), 'all'],
67 options_for_select([[l(:label_all_time), 'all'],
62 [l(:label_today), 'today'],
68 [l(:label_today), 'today'],
63 [l(:label_yesterday), 'yesterday'],
69 [l(:label_yesterday), 'yesterday'],
64 [l(:label_this_week), 'current_week'],
70 [l(:label_this_week), 'current_week'],
65 [l(:label_last_week), 'last_week'],
71 [l(:label_last_week), 'last_week'],
66 [l(:label_last_n_days, 7), '7_days'],
72 [l(:label_last_n_days, 7), '7_days'],
67 [l(:label_this_month), 'current_month'],
73 [l(:label_this_month), 'current_month'],
68 [l(:label_last_month), 'last_month'],
74 [l(:label_last_month), 'last_month'],
69 [l(:label_last_n_days, 30), '30_days'],
75 [l(:label_last_n_days, 30), '30_days'],
70 [l(:label_this_year), 'current_year']],
76 [l(:label_this_year), 'current_year']],
71 value)
77 value)
72 end
78 end
73
79
74 def entries_to_csv(entries)
80 def entries_to_csv(entries)
75 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
81 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
76 decimal_separator = l(:general_csv_decimal_separator)
82 decimal_separator = l(:general_csv_decimal_separator)
77 custom_fields = TimeEntryCustomField.find(:all)
83 custom_fields = TimeEntryCustomField.find(:all)
78 export = StringIO.new
84 export = StringIO.new
79 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
85 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
80 # csv header fields
86 # csv header fields
81 headers = [l(:field_spent_on),
87 headers = [l(:field_spent_on),
82 l(:field_user),
88 l(:field_user),
83 l(:field_activity),
89 l(:field_activity),
84 l(:field_project),
90 l(:field_project),
85 l(:field_issue),
91 l(:field_issue),
86 l(:field_tracker),
92 l(:field_tracker),
87 l(:field_subject),
93 l(:field_subject),
88 l(:field_hours),
94 l(:field_hours),
89 l(:field_comments)
95 l(:field_comments)
90 ]
96 ]
91 # Export custom fields
97 # Export custom fields
92 headers += custom_fields.collect(&:name)
98 headers += custom_fields.collect(&:name)
93
99
94 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
100 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
95 # csv lines
101 # csv lines
96 entries.each do |entry|
102 entries.each do |entry|
97 fields = [format_date(entry.spent_on),
103 fields = [format_date(entry.spent_on),
98 entry.user,
104 entry.user,
99 entry.activity,
105 entry.activity,
100 entry.project,
106 entry.project,
101 (entry.issue ? entry.issue.id : nil),
107 (entry.issue ? entry.issue.id : nil),
102 (entry.issue ? entry.issue.tracker : nil),
108 (entry.issue ? entry.issue.tracker : nil),
103 (entry.issue ? entry.issue.subject : nil),
109 (entry.issue ? entry.issue.subject : nil),
104 entry.hours.to_s.gsub('.', decimal_separator),
110 entry.hours.to_s.gsub('.', decimal_separator),
105 entry.comments
111 entry.comments
106 ]
112 ]
107 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
113 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
108
114
109 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
115 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
110 end
116 end
111 end
117 end
112 export.rewind
118 export.rewind
113 export
119 export
114 end
120 end
115
121
116 def format_criteria_value(criteria, value)
122 def format_criteria_value(criteria, value)
117 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
123 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
118 end
124 end
119
125
120 def report_to_csv(criterias, periods, hours)
126 def report_to_csv(criterias, periods, hours)
121 export = StringIO.new
127 export = StringIO.new
122 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
128 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
123 # Column headers
129 # Column headers
124 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
130 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
125 headers += periods
131 headers += periods
126 headers << l(:label_total)
132 headers << l(:label_total)
127 csv << headers.collect {|c| to_utf8(c) }
133 csv << headers.collect {|c| to_utf8(c) }
128 # Content
134 # Content
129 report_criteria_to_csv(csv, criterias, periods, hours)
135 report_criteria_to_csv(csv, criterias, periods, hours)
130 # Total row
136 # Total row
131 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
137 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
132 total = 0
138 total = 0
133 periods.each do |period|
139 periods.each do |period|
134 sum = sum_hours(select_hours(hours, @columns, period.to_s))
140 sum = sum_hours(select_hours(hours, @columns, period.to_s))
135 total += sum
141 total += sum
136 row << (sum > 0 ? "%.2f" % sum : '')
142 row << (sum > 0 ? "%.2f" % sum : '')
137 end
143 end
138 row << "%.2f" %total
144 row << "%.2f" %total
139 csv << row
145 csv << row
140 end
146 end
141 export.rewind
147 export.rewind
142 export
148 export
143 end
149 end
144
150
145 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
151 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
146 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
152 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
147 hours_for_value = select_hours(hours, criterias[level], value)
153 hours_for_value = select_hours(hours, criterias[level], value)
148 next if hours_for_value.empty?
154 next if hours_for_value.empty?
149 row = [''] * level
155 row = [''] * level
150 row << to_utf8(format_criteria_value(criterias[level], value))
156 row << to_utf8(format_criteria_value(criterias[level], value))
151 row += [''] * (criterias.length - level - 1)
157 row += [''] * (criterias.length - level - 1)
152 total = 0
158 total = 0
153 periods.each do |period|
159 periods.each do |period|
154 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
160 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
155 total += sum
161 total += sum
156 row << (sum > 0 ? "%.2f" % sum : '')
162 row << (sum > 0 ? "%.2f" % sum : '')
157 end
163 end
158 row << "%.2f" %total
164 row << "%.2f" %total
159 csv << row
165 csv << row
160
166
161 if criterias.length > level + 1
167 if criterias.length > level + 1
162 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
168 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
163 end
169 end
164 end
170 end
165 end
171 end
166
172
167 def to_utf8(s)
173 def to_utf8(s)
168 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
174 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
169 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
175 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
170 end
176 end
171 end
177 end
@@ -1,148 +1,148
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Enumeration < ActiveRecord::Base
18 class Enumeration < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20
20
21 acts_as_list :scope => 'type = \'#{type}\''
21 acts_as_list :scope => 'type = \'#{type}\''
22 acts_as_customizable
22 acts_as_customizable
23 acts_as_tree :order => 'position ASC'
23 acts_as_tree :order => 'position ASC'
24
24
25 before_destroy :check_integrity
25 before_destroy :check_integrity
26
26
27 validates_presence_of :name
27 validates_presence_of :name
28 validates_uniqueness_of :name, :scope => [:type]
28 validates_uniqueness_of :name, :scope => [:type]
29 validates_length_of :name, :maximum => 30
29 validates_length_of :name, :maximum => 30
30
30
31 # Backwards compatiblity named_scopes.
31 # Backwards compatiblity named_scopes.
32 # Can be removed post-0.9
32 # Can be removed post-0.9
33 named_scope :priorities, :conditions => { :type => "IssuePriority" }, :order => 'position' do
33 named_scope :priorities, :conditions => { :type => "IssuePriority" }, :order => 'position' do
34 ActiveSupport::Deprecation.warn("Enumeration#priorities is deprecated, use the IssuePriority class. (#{Redmine::Info.issue(3007)})")
34 ActiveSupport::Deprecation.warn("Enumeration#priorities is deprecated, use the IssuePriority class. (#{Redmine::Info.issue(3007)})")
35 def default
35 def default
36 find(:first, :conditions => { :is_default => true })
36 find(:first, :conditions => { :is_default => true })
37 end
37 end
38 end
38 end
39
39
40 named_scope :document_categories, :conditions => { :type => "DocumentCategory" }, :order => 'position' do
40 named_scope :document_categories, :conditions => { :type => "DocumentCategory" }, :order => 'position' do
41 ActiveSupport::Deprecation.warn("Enumeration#document_categories is deprecated, use the DocumentCategories class. (#{Redmine::Info.issue(3007)})")
41 ActiveSupport::Deprecation.warn("Enumeration#document_categories is deprecated, use the DocumentCategories class. (#{Redmine::Info.issue(3007)})")
42 def default
42 def default
43 find(:first, :conditions => { :is_default => true })
43 find(:first, :conditions => { :is_default => true })
44 end
44 end
45 end
45 end
46
46
47 named_scope :activities, :conditions => { :type => "TimeEntryActivity" }, :order => 'position' do
47 named_scope :activities, :conditions => { :type => "TimeEntryActivity" }, :order => 'position' do
48 ActiveSupport::Deprecation.warn("Enumeration#activities is deprecated, use the TimeEntryActivity class. (#{Redmine::Info.issue(3007)})")
48 ActiveSupport::Deprecation.warn("Enumeration#activities is deprecated, use the TimeEntryActivity class. (#{Redmine::Info.issue(3007)})")
49 def default
49 def default
50 find(:first, :conditions => { :is_default => true })
50 find(:first, :conditions => { :is_default => true })
51 end
51 end
52 end
52 end
53
53
54 named_scope :values, lambda {|type| { :conditions => { :type => type }, :order => 'position' } } do
54 named_scope :values, lambda {|type| { :conditions => { :type => type }, :order => 'position' } } do
55 def default
55 def default
56 find(:first, :conditions => { :is_default => true })
56 find(:first, :conditions => { :is_default => true })
57 end
57 end
58 end
58 end
59 # End backwards compatiblity named_scopes
59 # End backwards compatiblity named_scopes
60
60
61 named_scope :all, :order => 'position'
61 named_scope :all, :order => 'position'
62
62
63 named_scope :active, lambda {
63 named_scope :active, lambda {
64 {
64 {
65 :conditions => {:active => true},
65 :conditions => {:active => true, :project_id => nil},
66 :order => 'position'
66 :order => 'position'
67 }
67 }
68 }
68 }
69
69
70 def self.default
70 def self.default
71 # Creates a fake default scope so Enumeration.default will check
71 # Creates a fake default scope so Enumeration.default will check
72 # it's type. STI subclasses will automatically add their own
72 # it's type. STI subclasses will automatically add their own
73 # types to the finder.
73 # types to the finder.
74 if self.descends_from_active_record?
74 if self.descends_from_active_record?
75 find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
75 find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
76 else
76 else
77 # STI classes are
77 # STI classes are
78 find(:first, :conditions => { :is_default => true })
78 find(:first, :conditions => { :is_default => true })
79 end
79 end
80 end
80 end
81
81
82 # Overloaded on concrete classes
82 # Overloaded on concrete classes
83 def option_name
83 def option_name
84 nil
84 nil
85 end
85 end
86
86
87 # Backwards compatiblity. Can be removed post-0.9
87 # Backwards compatiblity. Can be removed post-0.9
88 def opt
88 def opt
89 ActiveSupport::Deprecation.warn("Enumeration#opt is deprecated, use the STI classes now. (#{Redmine::Info.issue(3007)})")
89 ActiveSupport::Deprecation.warn("Enumeration#opt is deprecated, use the STI classes now. (#{Redmine::Info.issue(3007)})")
90 return OptName
90 return OptName
91 end
91 end
92
92
93 def before_save
93 def before_save
94 if is_default? && is_default_changed?
94 if is_default? && is_default_changed?
95 Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
95 Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
96 end
96 end
97 end
97 end
98
98
99 # Overloaded on concrete classes
99 # Overloaded on concrete classes
100 def objects_count
100 def objects_count
101 0
101 0
102 end
102 end
103
103
104 def in_use?
104 def in_use?
105 self.objects_count != 0
105 self.objects_count != 0
106 end
106 end
107
107
108 # Is this enumeration overiding a system level enumeration?
108 # Is this enumeration overiding a system level enumeration?
109 def is_override?
109 def is_override?
110 !self.parent.nil?
110 !self.parent.nil?
111 end
111 end
112
112
113 alias :destroy_without_reassign :destroy
113 alias :destroy_without_reassign :destroy
114
114
115 # Destroy the enumeration
115 # Destroy the enumeration
116 # If a enumeration is specified, objects are reassigned
116 # If a enumeration is specified, objects are reassigned
117 def destroy(reassign_to = nil)
117 def destroy(reassign_to = nil)
118 if reassign_to && reassign_to.is_a?(Enumeration)
118 if reassign_to && reassign_to.is_a?(Enumeration)
119 self.transfer_relations(reassign_to)
119 self.transfer_relations(reassign_to)
120 end
120 end
121 destroy_without_reassign
121 destroy_without_reassign
122 end
122 end
123
123
124 def <=>(enumeration)
124 def <=>(enumeration)
125 position <=> enumeration.position
125 position <=> enumeration.position
126 end
126 end
127
127
128 def to_s; name end
128 def to_s; name end
129
129
130 # Returns the Subclasses of Enumeration. Each Subclass needs to be
130 # Returns the Subclasses of Enumeration. Each Subclass needs to be
131 # required in development mode.
131 # required in development mode.
132 #
132 #
133 # Note: subclasses is protected in ActiveRecord
133 # Note: subclasses is protected in ActiveRecord
134 def self.get_subclasses
134 def self.get_subclasses
135 @@subclasses[Enumeration]
135 @@subclasses[Enumeration]
136 end
136 end
137
137
138 private
138 private
139 def check_integrity
139 def check_integrity
140 raise "Can't delete enumeration" if self.in_use?
140 raise "Can't delete enumeration" if self.in_use?
141 end
141 end
142
142
143 end
143 end
144
144
145 # Force load the subclasses in development mode
145 # Force load the subclasses in development mode
146 require_dependency 'time_entry_activity'
146 require_dependency 'time_entry_activity'
147 require_dependency 'document_category'
147 require_dependency 'document_category'
148 require_dependency 'issue_priority'
148 require_dependency 'issue_priority'
@@ -1,449 +1,469
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :time_entry_activities, :conditions => {:active => true } # Specific overidden Activities
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :member_principals, :class_name => 'Member',
25 has_many :member_principals, :class_name => 'Member',
25 :include => :principal,
26 :include => :principal,
26 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
27 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
27 has_many :users, :through => :members
28 has_many :users, :through => :members
28 has_many :principals, :through => :member_principals, :source => :principal
29 has_many :principals, :through => :member_principals, :source => :principal
29
30
30 has_many :enabled_modules, :dependent => :delete_all
31 has_many :enabled_modules, :dependent => :delete_all
31 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
32 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
32 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
33 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
33 has_many :issue_changes, :through => :issues, :source => :journals
34 has_many :issue_changes, :through => :issues, :source => :journals
34 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
35 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
35 has_many :time_entries, :dependent => :delete_all
36 has_many :time_entries, :dependent => :delete_all
36 has_many :queries, :dependent => :delete_all
37 has_many :queries, :dependent => :delete_all
37 has_many :documents, :dependent => :destroy
38 has_many :documents, :dependent => :destroy
38 has_many :news, :dependent => :delete_all, :include => :author
39 has_many :news, :dependent => :delete_all, :include => :author
39 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
40 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
40 has_many :boards, :dependent => :destroy, :order => "position ASC"
41 has_many :boards, :dependent => :destroy, :order => "position ASC"
41 has_one :repository, :dependent => :destroy
42 has_one :repository, :dependent => :destroy
42 has_many :changesets, :through => :repository
43 has_many :changesets, :through => :repository
43 has_one :wiki, :dependent => :destroy
44 has_one :wiki, :dependent => :destroy
44 # Custom field for the project issues
45 # Custom field for the project issues
45 has_and_belongs_to_many :issue_custom_fields,
46 has_and_belongs_to_many :issue_custom_fields,
46 :class_name => 'IssueCustomField',
47 :class_name => 'IssueCustomField',
47 :order => "#{CustomField.table_name}.position",
48 :order => "#{CustomField.table_name}.position",
48 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
49 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
49 :association_foreign_key => 'custom_field_id'
50 :association_foreign_key => 'custom_field_id'
50
51
51 acts_as_nested_set :order => 'name', :dependent => :destroy
52 acts_as_nested_set :order => 'name', :dependent => :destroy
52 acts_as_attachable :view_permission => :view_files,
53 acts_as_attachable :view_permission => :view_files,
53 :delete_permission => :manage_files
54 :delete_permission => :manage_files
54
55
55 acts_as_customizable
56 acts_as_customizable
56 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
57 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
57 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
58 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
58 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
59 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
59 :author => nil
60 :author => nil
60
61
61 attr_protected :status, :enabled_module_names
62 attr_protected :status, :enabled_module_names
62
63
63 validates_presence_of :name, :identifier
64 validates_presence_of :name, :identifier
64 validates_uniqueness_of :name, :identifier
65 validates_uniqueness_of :name, :identifier
65 validates_associated :repository, :wiki
66 validates_associated :repository, :wiki
66 validates_length_of :name, :maximum => 30
67 validates_length_of :name, :maximum => 30
67 validates_length_of :homepage, :maximum => 255
68 validates_length_of :homepage, :maximum => 255
68 validates_length_of :identifier, :in => 1..20
69 validates_length_of :identifier, :in => 1..20
69 # donwcase letters, digits, dashes but not digits only
70 # donwcase letters, digits, dashes but not digits only
70 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
71 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
71 # reserved words
72 # reserved words
72 validates_exclusion_of :identifier, :in => %w( new )
73 validates_exclusion_of :identifier, :in => %w( new )
73
74
74 before_destroy :delete_all_members
75 before_destroy :delete_all_members
75
76
76 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
77 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
77 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
78 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
78 named_scope :all_public, { :conditions => { :is_public => true } }
79 named_scope :all_public, { :conditions => { :is_public => true } }
79 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
80 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
80
81
81 def identifier=(identifier)
82 def identifier=(identifier)
82 super unless identifier_frozen?
83 super unless identifier_frozen?
83 end
84 end
84
85
85 def identifier_frozen?
86 def identifier_frozen?
86 errors[:identifier].nil? && !(new_record? || identifier.blank?)
87 errors[:identifier].nil? && !(new_record? || identifier.blank?)
87 end
88 end
88
89
89 def issues_with_subprojects(include_subprojects=false)
90 def issues_with_subprojects(include_subprojects=false)
90 conditions = nil
91 conditions = nil
91 if include_subprojects
92 if include_subprojects
92 ids = [id] + descendants.collect(&:id)
93 ids = [id] + descendants.collect(&:id)
93 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
94 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
94 end
95 end
95 conditions ||= ["#{Project.table_name}.id = ?", id]
96 conditions ||= ["#{Project.table_name}.id = ?", id]
96 # Quick and dirty fix for Rails 2 compatibility
97 # Quick and dirty fix for Rails 2 compatibility
97 Issue.send(:with_scope, :find => { :conditions => conditions }) do
98 Issue.send(:with_scope, :find => { :conditions => conditions }) do
98 Version.send(:with_scope, :find => { :conditions => conditions }) do
99 Version.send(:with_scope, :find => { :conditions => conditions }) do
99 yield
100 yield
100 end
101 end
101 end
102 end
102 end
103 end
103
104
104 # returns latest created projects
105 # returns latest created projects
105 # non public projects will be returned only if user is a member of those
106 # non public projects will be returned only if user is a member of those
106 def self.latest(user=nil, count=5)
107 def self.latest(user=nil, count=5)
107 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
108 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
108 end
109 end
109
110
110 # Returns a SQL :conditions string used to find all active projects for the specified user.
111 # Returns a SQL :conditions string used to find all active projects for the specified user.
111 #
112 #
112 # Examples:
113 # Examples:
113 # Projects.visible_by(admin) => "projects.status = 1"
114 # Projects.visible_by(admin) => "projects.status = 1"
114 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
115 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
115 def self.visible_by(user=nil)
116 def self.visible_by(user=nil)
116 user ||= User.current
117 user ||= User.current
117 if user && user.admin?
118 if user && user.admin?
118 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
119 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
119 elsif user && user.memberships.any?
120 elsif user && user.memberships.any?
120 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
121 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
121 else
122 else
122 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
123 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
123 end
124 end
124 end
125 end
125
126
126 def self.allowed_to_condition(user, permission, options={})
127 def self.allowed_to_condition(user, permission, options={})
127 statements = []
128 statements = []
128 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 if perm = Redmine::AccessControl.permission(permission)
130 if perm = Redmine::AccessControl.permission(permission)
130 unless perm.project_module.nil?
131 unless perm.project_module.nil?
131 # If the permission belongs to a project module, make sure the module is enabled
132 # If the permission belongs to a project module, make sure the module is enabled
132 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
133 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
133 end
134 end
134 end
135 end
135 if options[:project]
136 if options[:project]
136 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
137 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
137 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
138 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
138 base_statement = "(#{project_statement}) AND (#{base_statement})"
139 base_statement = "(#{project_statement}) AND (#{base_statement})"
139 end
140 end
140 if user.admin?
141 if user.admin?
141 # no restriction
142 # no restriction
142 else
143 else
143 statements << "1=0"
144 statements << "1=0"
144 if user.logged?
145 if user.logged?
145 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
146 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
146 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
147 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
147 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
148 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
148 elsif Role.anonymous.allowed_to?(permission)
149 elsif Role.anonymous.allowed_to?(permission)
149 # anonymous user allowed on public project
150 # anonymous user allowed on public project
150 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
151 else
152 else
152 # anonymous user is not authorized
153 # anonymous user is not authorized
153 end
154 end
154 end
155 end
155 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
156 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
156 end
157 end
157
158
159 # Returns all the Systemwide and project specific activities
160 def activities
161 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
162
163 if overridden_activity_ids.empty?
164 return TimeEntryActivity.active
165 else
166 return system_activities_and_project_overrides
167 end
168 end
169
158 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
170 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
159 #
171 #
160 # Examples:
172 # Examples:
161 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
173 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
162 # project.project_condition(false) => "projects.id = 1"
174 # project.project_condition(false) => "projects.id = 1"
163 def project_condition(with_subprojects)
175 def project_condition(with_subprojects)
164 cond = "#{Project.table_name}.id = #{id}"
176 cond = "#{Project.table_name}.id = #{id}"
165 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
177 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
166 cond
178 cond
167 end
179 end
168
180
169 def self.find(*args)
181 def self.find(*args)
170 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
182 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
171 project = find_by_identifier(*args)
183 project = find_by_identifier(*args)
172 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
184 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
173 project
185 project
174 else
186 else
175 super
187 super
176 end
188 end
177 end
189 end
178
190
179 def to_param
191 def to_param
180 # id is used for projects with a numeric identifier (compatibility)
192 # id is used for projects with a numeric identifier (compatibility)
181 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
193 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
182 end
194 end
183
195
184 def active?
196 def active?
185 self.status == STATUS_ACTIVE
197 self.status == STATUS_ACTIVE
186 end
198 end
187
199
188 # Archives the project and its descendants recursively
200 # Archives the project and its descendants recursively
189 def archive
201 def archive
190 # Archive subprojects if any
202 # Archive subprojects if any
191 children.each do |subproject|
203 children.each do |subproject|
192 subproject.archive
204 subproject.archive
193 end
205 end
194 update_attribute :status, STATUS_ARCHIVED
206 update_attribute :status, STATUS_ARCHIVED
195 end
207 end
196
208
197 # Unarchives the project
209 # Unarchives the project
198 # All its ancestors must be active
210 # All its ancestors must be active
199 def unarchive
211 def unarchive
200 return false if ancestors.detect {|a| !a.active?}
212 return false if ancestors.detect {|a| !a.active?}
201 update_attribute :status, STATUS_ACTIVE
213 update_attribute :status, STATUS_ACTIVE
202 end
214 end
203
215
204 # Returns an array of projects the project can be moved to
216 # Returns an array of projects the project can be moved to
205 def possible_parents
217 def possible_parents
206 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
218 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
207 end
219 end
208
220
209 # Sets the parent of the project
221 # Sets the parent of the project
210 # Argument can be either a Project, a String, a Fixnum or nil
222 # Argument can be either a Project, a String, a Fixnum or nil
211 def set_parent!(p)
223 def set_parent!(p)
212 unless p.nil? || p.is_a?(Project)
224 unless p.nil? || p.is_a?(Project)
213 if p.to_s.blank?
225 if p.to_s.blank?
214 p = nil
226 p = nil
215 else
227 else
216 p = Project.find_by_id(p)
228 p = Project.find_by_id(p)
217 return false unless p
229 return false unless p
218 end
230 end
219 end
231 end
220 if p == parent && !p.nil?
232 if p == parent && !p.nil?
221 # Nothing to do
233 # Nothing to do
222 true
234 true
223 elsif p.nil? || (p.active? && move_possible?(p))
235 elsif p.nil? || (p.active? && move_possible?(p))
224 # Insert the project so that target's children or root projects stay alphabetically sorted
236 # Insert the project so that target's children or root projects stay alphabetically sorted
225 sibs = (p.nil? ? self.class.roots : p.children)
237 sibs = (p.nil? ? self.class.roots : p.children)
226 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
238 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
227 if to_be_inserted_before
239 if to_be_inserted_before
228 move_to_left_of(to_be_inserted_before)
240 move_to_left_of(to_be_inserted_before)
229 elsif p.nil?
241 elsif p.nil?
230 if sibs.empty?
242 if sibs.empty?
231 # move_to_root adds the project in first (ie. left) position
243 # move_to_root adds the project in first (ie. left) position
232 move_to_root
244 move_to_root
233 else
245 else
234 move_to_right_of(sibs.last) unless self == sibs.last
246 move_to_right_of(sibs.last) unless self == sibs.last
235 end
247 end
236 else
248 else
237 # move_to_child_of adds the project in last (ie.right) position
249 # move_to_child_of adds the project in last (ie.right) position
238 move_to_child_of(p)
250 move_to_child_of(p)
239 end
251 end
240 true
252 true
241 else
253 else
242 # Can not move to the given target
254 # Can not move to the given target
243 false
255 false
244 end
256 end
245 end
257 end
246
258
247 # Returns an array of the trackers used by the project and its active sub projects
259 # Returns an array of the trackers used by the project and its active sub projects
248 def rolled_up_trackers
260 def rolled_up_trackers
249 @rolled_up_trackers ||=
261 @rolled_up_trackers ||=
250 Tracker.find(:all, :include => :projects,
262 Tracker.find(:all, :include => :projects,
251 :select => "DISTINCT #{Tracker.table_name}.*",
263 :select => "DISTINCT #{Tracker.table_name}.*",
252 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
264 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
253 :order => "#{Tracker.table_name}.position")
265 :order => "#{Tracker.table_name}.position")
254 end
266 end
255
267
256 # Returns a hash of project users grouped by role
268 # Returns a hash of project users grouped by role
257 def users_by_role
269 def users_by_role
258 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
270 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
259 m.roles.each do |r|
271 m.roles.each do |r|
260 h[r] ||= []
272 h[r] ||= []
261 h[r] << m.user
273 h[r] << m.user
262 end
274 end
263 h
275 h
264 end
276 end
265 end
277 end
266
278
267 # Deletes all project's members
279 # Deletes all project's members
268 def delete_all_members
280 def delete_all_members
269 me, mr = Member.table_name, MemberRole.table_name
281 me, mr = Member.table_name, MemberRole.table_name
270 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
282 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
271 Member.delete_all(['project_id = ?', id])
283 Member.delete_all(['project_id = ?', id])
272 end
284 end
273
285
274 # Users issues can be assigned to
286 # Users issues can be assigned to
275 def assignable_users
287 def assignable_users
276 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
288 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
277 end
289 end
278
290
279 # Returns the mail adresses of users that should be always notified on project events
291 # Returns the mail adresses of users that should be always notified on project events
280 def recipients
292 def recipients
281 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
293 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
282 end
294 end
283
295
284 # Returns an array of all custom fields enabled for project issues
296 # Returns an array of all custom fields enabled for project issues
285 # (explictly associated custom fields and custom fields enabled for all projects)
297 # (explictly associated custom fields and custom fields enabled for all projects)
286 def all_issue_custom_fields
298 def all_issue_custom_fields
287 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
299 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
288 end
300 end
289
301
290 def project
302 def project
291 self
303 self
292 end
304 end
293
305
294 def <=>(project)
306 def <=>(project)
295 name.downcase <=> project.name.downcase
307 name.downcase <=> project.name.downcase
296 end
308 end
297
309
298 def to_s
310 def to_s
299 name
311 name
300 end
312 end
301
313
302 # Returns a short description of the projects (first lines)
314 # Returns a short description of the projects (first lines)
303 def short_description(length = 255)
315 def short_description(length = 255)
304 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
316 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
305 end
317 end
306
318
307 # Return true if this project is allowed to do the specified action.
319 # Return true if this project is allowed to do the specified action.
308 # action can be:
320 # action can be:
309 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
321 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
310 # * a permission Symbol (eg. :edit_project)
322 # * a permission Symbol (eg. :edit_project)
311 def allows_to?(action)
323 def allows_to?(action)
312 if action.is_a? Hash
324 if action.is_a? Hash
313 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
325 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
314 else
326 else
315 allowed_permissions.include? action
327 allowed_permissions.include? action
316 end
328 end
317 end
329 end
318
330
319 def module_enabled?(module_name)
331 def module_enabled?(module_name)
320 module_name = module_name.to_s
332 module_name = module_name.to_s
321 enabled_modules.detect {|m| m.name == module_name}
333 enabled_modules.detect {|m| m.name == module_name}
322 end
334 end
323
335
324 def enabled_module_names=(module_names)
336 def enabled_module_names=(module_names)
325 if module_names && module_names.is_a?(Array)
337 if module_names && module_names.is_a?(Array)
326 module_names = module_names.collect(&:to_s)
338 module_names = module_names.collect(&:to_s)
327 # remove disabled modules
339 # remove disabled modules
328 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
340 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
329 # add new modules
341 # add new modules
330 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
342 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
331 else
343 else
332 enabled_modules.clear
344 enabled_modules.clear
333 end
345 end
334 end
346 end
335
347
336 # Returns an auto-generated project identifier based on the last identifier used
348 # Returns an auto-generated project identifier based on the last identifier used
337 def self.next_identifier
349 def self.next_identifier
338 p = Project.find(:first, :order => 'created_on DESC')
350 p = Project.find(:first, :order => 'created_on DESC')
339 p.nil? ? nil : p.identifier.to_s.succ
351 p.nil? ? nil : p.identifier.to_s.succ
340 end
352 end
341
353
342 # Copies and saves the Project instance based on the +project+.
354 # Copies and saves the Project instance based on the +project+.
343 # Will duplicate the source project's:
355 # Will duplicate the source project's:
344 # * Issues
356 # * Issues
345 # * Members
357 # * Members
346 # * Queries
358 # * Queries
347 def copy(project)
359 def copy(project)
348 project = project.is_a?(Project) ? project : Project.find(project)
360 project = project.is_a?(Project) ? project : Project.find(project)
349
361
350 Project.transaction do
362 Project.transaction do
351 # Wikis
363 # Wikis
352 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
364 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
353 project.wiki.pages.each do |page|
365 project.wiki.pages.each do |page|
354 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
366 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
355 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
367 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
356 new_wiki_page.content = new_wiki_content
368 new_wiki_page.content = new_wiki_content
357
369
358 self.wiki.pages << new_wiki_page
370 self.wiki.pages << new_wiki_page
359 end
371 end
360
372
361 # Versions
373 # Versions
362 project.versions.each do |version|
374 project.versions.each do |version|
363 new_version = Version.new
375 new_version = Version.new
364 new_version.attributes = version.attributes.dup.except("project_id")
376 new_version.attributes = version.attributes.dup.except("project_id")
365 self.versions << new_version
377 self.versions << new_version
366 end
378 end
367
379
368 project.issue_categories.each do |issue_category|
380 project.issue_categories.each do |issue_category|
369 new_issue_category = IssueCategory.new
381 new_issue_category = IssueCategory.new
370 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
382 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
371 self.issue_categories << new_issue_category
383 self.issue_categories << new_issue_category
372 end
384 end
373
385
374 # Issues
386 # Issues
375 project.issues.each do |issue|
387 project.issues.each do |issue|
376 new_issue = Issue.new
388 new_issue = Issue.new
377 new_issue.copy_from(issue)
389 new_issue.copy_from(issue)
378 # Reassign fixed_versions by name, since names are unique per
390 # Reassign fixed_versions by name, since names are unique per
379 # project and the versions for self are not yet saved
391 # project and the versions for self are not yet saved
380 if issue.fixed_version
392 if issue.fixed_version
381 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
393 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
382 end
394 end
383 # Reassign the category by name, since names are unique per
395 # Reassign the category by name, since names are unique per
384 # project and the categories for self are not yet saved
396 # project and the categories for self are not yet saved
385 if issue.category
397 if issue.category
386 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
398 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
387 end
399 end
388
400
389 self.issues << new_issue
401 self.issues << new_issue
390 end
402 end
391
403
392 # Members
404 # Members
393 project.members.each do |member|
405 project.members.each do |member|
394 new_member = Member.new
406 new_member = Member.new
395 new_member.attributes = member.attributes.dup.except("project_id")
407 new_member.attributes = member.attributes.dup.except("project_id")
396 new_member.role_ids = member.role_ids.dup
408 new_member.role_ids = member.role_ids.dup
397 new_member.project = self
409 new_member.project = self
398 self.members << new_member
410 self.members << new_member
399 end
411 end
400
412
401 # Queries
413 # Queries
402 project.queries.each do |query|
414 project.queries.each do |query|
403 new_query = Query.new
415 new_query = Query.new
404 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
416 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
405 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
417 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
406 new_query.project = self
418 new_query.project = self
407 self.queries << new_query
419 self.queries << new_query
408 end
420 end
409
421
410 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
422 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
411 self.save
423 self.save
412 end
424 end
413 end
425 end
414
426
415
427
416 # Copies +project+ and returns the new instance. This will not save
428 # Copies +project+ and returns the new instance. This will not save
417 # the copy
429 # the copy
418 def self.copy_from(project)
430 def self.copy_from(project)
419 begin
431 begin
420 project = project.is_a?(Project) ? project : Project.find(project)
432 project = project.is_a?(Project) ? project : Project.find(project)
421 if project
433 if project
422 # clear unique attributes
434 # clear unique attributes
423 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
435 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
424 copy = Project.new(attributes)
436 copy = Project.new(attributes)
425 copy.enabled_modules = project.enabled_modules
437 copy.enabled_modules = project.enabled_modules
426 copy.trackers = project.trackers
438 copy.trackers = project.trackers
427 copy.custom_values = project.custom_values.collect {|v| v.clone}
439 copy.custom_values = project.custom_values.collect {|v| v.clone}
428 copy.issue_custom_fields = project.issue_custom_fields
440 copy.issue_custom_fields = project.issue_custom_fields
429 return copy
441 return copy
430 else
442 else
431 return nil
443 return nil
432 end
444 end
433 rescue ActiveRecord::RecordNotFound
445 rescue ActiveRecord::RecordNotFound
434 return nil
446 return nil
435 end
447 end
436 end
448 end
437
449
438 private
450 private
439 def allowed_permissions
451 def allowed_permissions
440 @allowed_permissions ||= begin
452 @allowed_permissions ||= begin
441 module_names = enabled_modules.collect {|m| m.name}
453 module_names = enabled_modules.collect {|m| m.name}
442 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
454 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
443 end
455 end
444 end
456 end
445
457
446 def allowed_actions
458 def allowed_actions
447 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
459 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
448 end
460 end
461
462 # Returns the systemwide activities merged with the project specific overrides
463 def system_activities_and_project_overrides
464 return TimeEntryActivity.active.
465 find(:all,
466 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
467 self.time_entry_activities
468 end
449 end
469 end
@@ -1,445 +1,492
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :issues, :issue_statuses, :journals, :journal_details,
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
24 :queries
25
25
26 def setup
26 def setup
27 @ecookbook = Project.find(1)
27 @ecookbook = Project.find(1)
28 @ecookbook_sub1 = Project.find(3)
28 @ecookbook_sub1 = Project.find(3)
29 end
29 end
30
30
31 should_validate_presence_of :name
31 should_validate_presence_of :name
32 should_validate_presence_of :identifier
32 should_validate_presence_of :identifier
33
33
34 should_validate_uniqueness_of :name
34 should_validate_uniqueness_of :name
35 should_validate_uniqueness_of :identifier
35 should_validate_uniqueness_of :identifier
36
36
37 context "associations" do
37 context "associations" do
38 should_have_many :members
38 should_have_many :members
39 should_have_many :users, :through => :members
39 should_have_many :users, :through => :members
40 should_have_many :member_principals
40 should_have_many :member_principals
41 should_have_many :principals, :through => :member_principals
41 should_have_many :principals, :through => :member_principals
42 should_have_many :enabled_modules
42 should_have_many :enabled_modules
43 should_have_many :issues
43 should_have_many :issues
44 should_have_many :issue_changes, :through => :issues
44 should_have_many :issue_changes, :through => :issues
45 should_have_many :versions
45 should_have_many :versions
46 should_have_many :time_entries
46 should_have_many :time_entries
47 should_have_many :queries
47 should_have_many :queries
48 should_have_many :documents
48 should_have_many :documents
49 should_have_many :news
49 should_have_many :news
50 should_have_many :issue_categories
50 should_have_many :issue_categories
51 should_have_many :boards
51 should_have_many :boards
52 should_have_many :changesets, :through => :repository
52 should_have_many :changesets, :through => :repository
53
53
54 should_have_one :repository
54 should_have_one :repository
55 should_have_one :wiki
55 should_have_one :wiki
56
56
57 should_have_and_belong_to_many :trackers
57 should_have_and_belong_to_many :trackers
58 should_have_and_belong_to_many :issue_custom_fields
58 should_have_and_belong_to_many :issue_custom_fields
59 end
59 end
60
60
61 def test_truth
61 def test_truth
62 assert_kind_of Project, @ecookbook
62 assert_kind_of Project, @ecookbook
63 assert_equal "eCookbook", @ecookbook.name
63 assert_equal "eCookbook", @ecookbook.name
64 end
64 end
65
65
66 def test_update
66 def test_update
67 assert_equal "eCookbook", @ecookbook.name
67 assert_equal "eCookbook", @ecookbook.name
68 @ecookbook.name = "eCook"
68 @ecookbook.name = "eCook"
69 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
69 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
70 @ecookbook.reload
70 @ecookbook.reload
71 assert_equal "eCook", @ecookbook.name
71 assert_equal "eCook", @ecookbook.name
72 end
72 end
73
73
74 def test_validate_identifier
74 def test_validate_identifier
75 to_test = {"abc" => true,
75 to_test = {"abc" => true,
76 "ab12" => true,
76 "ab12" => true,
77 "ab-12" => true,
77 "ab-12" => true,
78 "12" => false,
78 "12" => false,
79 "new" => false}
79 "new" => false}
80
80
81 to_test.each do |identifier, valid|
81 to_test.each do |identifier, valid|
82 p = Project.new
82 p = Project.new
83 p.identifier = identifier
83 p.identifier = identifier
84 p.valid?
84 p.valid?
85 assert_equal valid, p.errors.on('identifier').nil?
85 assert_equal valid, p.errors.on('identifier').nil?
86 end
86 end
87 end
87 end
88
88
89 def test_members_should_be_active_users
89 def test_members_should_be_active_users
90 Project.all.each do |project|
90 Project.all.each do |project|
91 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
91 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
92 end
92 end
93 end
93 end
94
94
95 def test_users_should_be_active_users
95 def test_users_should_be_active_users
96 Project.all.each do |project|
96 Project.all.each do |project|
97 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
97 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
98 end
98 end
99 end
99 end
100
100
101 def test_archive
101 def test_archive
102 user = @ecookbook.members.first.user
102 user = @ecookbook.members.first.user
103 @ecookbook.archive
103 @ecookbook.archive
104 @ecookbook.reload
104 @ecookbook.reload
105
105
106 assert !@ecookbook.active?
106 assert !@ecookbook.active?
107 assert !user.projects.include?(@ecookbook)
107 assert !user.projects.include?(@ecookbook)
108 # Subproject are also archived
108 # Subproject are also archived
109 assert !@ecookbook.children.empty?
109 assert !@ecookbook.children.empty?
110 assert @ecookbook.descendants.active.empty?
110 assert @ecookbook.descendants.active.empty?
111 end
111 end
112
112
113 def test_unarchive
113 def test_unarchive
114 user = @ecookbook.members.first.user
114 user = @ecookbook.members.first.user
115 @ecookbook.archive
115 @ecookbook.archive
116 # A subproject of an archived project can not be unarchived
116 # A subproject of an archived project can not be unarchived
117 assert !@ecookbook_sub1.unarchive
117 assert !@ecookbook_sub1.unarchive
118
118
119 # Unarchive project
119 # Unarchive project
120 assert @ecookbook.unarchive
120 assert @ecookbook.unarchive
121 @ecookbook.reload
121 @ecookbook.reload
122 assert @ecookbook.active?
122 assert @ecookbook.active?
123 assert user.projects.include?(@ecookbook)
123 assert user.projects.include?(@ecookbook)
124 # Subproject can now be unarchived
124 # Subproject can now be unarchived
125 @ecookbook_sub1.reload
125 @ecookbook_sub1.reload
126 assert @ecookbook_sub1.unarchive
126 assert @ecookbook_sub1.unarchive
127 end
127 end
128
128
129 def test_destroy
129 def test_destroy
130 # 2 active members
130 # 2 active members
131 assert_equal 2, @ecookbook.members.size
131 assert_equal 2, @ecookbook.members.size
132 # and 1 is locked
132 # and 1 is locked
133 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
133 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
134 # some boards
134 # some boards
135 assert @ecookbook.boards.any?
135 assert @ecookbook.boards.any?
136
136
137 @ecookbook.destroy
137 @ecookbook.destroy
138 # make sure that the project non longer exists
138 # make sure that the project non longer exists
139 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
139 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
140 # make sure related data was removed
140 # make sure related data was removed
141 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
141 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 end
143 end
144
144
145 def test_move_an_orphan_project_to_a_root_project
145 def test_move_an_orphan_project_to_a_root_project
146 sub = Project.find(2)
146 sub = Project.find(2)
147 sub.set_parent! @ecookbook
147 sub.set_parent! @ecookbook
148 assert_equal @ecookbook.id, sub.parent.id
148 assert_equal @ecookbook.id, sub.parent.id
149 @ecookbook.reload
149 @ecookbook.reload
150 assert_equal 4, @ecookbook.children.size
150 assert_equal 4, @ecookbook.children.size
151 end
151 end
152
152
153 def test_move_an_orphan_project_to_a_subproject
153 def test_move_an_orphan_project_to_a_subproject
154 sub = Project.find(2)
154 sub = Project.find(2)
155 assert sub.set_parent!(@ecookbook_sub1)
155 assert sub.set_parent!(@ecookbook_sub1)
156 end
156 end
157
157
158 def test_move_a_root_project_to_a_project
158 def test_move_a_root_project_to_a_project
159 sub = @ecookbook
159 sub = @ecookbook
160 assert sub.set_parent!(Project.find(2))
160 assert sub.set_parent!(Project.find(2))
161 end
161 end
162
162
163 def test_should_not_move_a_project_to_its_children
163 def test_should_not_move_a_project_to_its_children
164 sub = @ecookbook
164 sub = @ecookbook
165 assert !(sub.set_parent!(Project.find(3)))
165 assert !(sub.set_parent!(Project.find(3)))
166 end
166 end
167
167
168 def test_set_parent_should_add_roots_in_alphabetical_order
168 def test_set_parent_should_add_roots_in_alphabetical_order
169 ProjectCustomField.delete_all
169 ProjectCustomField.delete_all
170 Project.delete_all
170 Project.delete_all
171 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
171 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
172 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
172 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
173 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
173 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
174 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
174 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
175
175
176 assert_equal 4, Project.count
176 assert_equal 4, Project.count
177 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
177 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
178 end
178 end
179
179
180 def test_set_parent_should_add_children_in_alphabetical_order
180 def test_set_parent_should_add_children_in_alphabetical_order
181 ProjectCustomField.delete_all
181 ProjectCustomField.delete_all
182 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
182 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
183 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
183 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
184 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
184 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
185 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
185 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
186 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
186 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
187
187
188 parent.reload
188 parent.reload
189 assert_equal 4, parent.children.size
189 assert_equal 4, parent.children.size
190 assert_equal parent.children.sort_by(&:name), parent.children
190 assert_equal parent.children.sort_by(&:name), parent.children
191 end
191 end
192
192
193 def test_rebuild_should_sort_children_alphabetically
193 def test_rebuild_should_sort_children_alphabetically
194 ProjectCustomField.delete_all
194 ProjectCustomField.delete_all
195 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
195 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
196 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
196 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
197 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
197 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
198 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
198 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
199 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
199 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
200
200
201 Project.update_all("lft = NULL, rgt = NULL")
201 Project.update_all("lft = NULL, rgt = NULL")
202 Project.rebuild!
202 Project.rebuild!
203
203
204 parent.reload
204 parent.reload
205 assert_equal 4, parent.children.size
205 assert_equal 4, parent.children.size
206 assert_equal parent.children.sort_by(&:name), parent.children
206 assert_equal parent.children.sort_by(&:name), parent.children
207 end
207 end
208
208
209 def test_parent
209 def test_parent
210 p = Project.find(6).parent
210 p = Project.find(6).parent
211 assert p.is_a?(Project)
211 assert p.is_a?(Project)
212 assert_equal 5, p.id
212 assert_equal 5, p.id
213 end
213 end
214
214
215 def test_ancestors
215 def test_ancestors
216 a = Project.find(6).ancestors
216 a = Project.find(6).ancestors
217 assert a.first.is_a?(Project)
217 assert a.first.is_a?(Project)
218 assert_equal [1, 5], a.collect(&:id)
218 assert_equal [1, 5], a.collect(&:id)
219 end
219 end
220
220
221 def test_root
221 def test_root
222 r = Project.find(6).root
222 r = Project.find(6).root
223 assert r.is_a?(Project)
223 assert r.is_a?(Project)
224 assert_equal 1, r.id
224 assert_equal 1, r.id
225 end
225 end
226
226
227 def test_children
227 def test_children
228 c = Project.find(1).children
228 c = Project.find(1).children
229 assert c.first.is_a?(Project)
229 assert c.first.is_a?(Project)
230 assert_equal [5, 3, 4], c.collect(&:id)
230 assert_equal [5, 3, 4], c.collect(&:id)
231 end
231 end
232
232
233 def test_descendants
233 def test_descendants
234 d = Project.find(1).descendants
234 d = Project.find(1).descendants
235 assert d.first.is_a?(Project)
235 assert d.first.is_a?(Project)
236 assert_equal [5, 6, 3, 4], d.collect(&:id)
236 assert_equal [5, 6, 3, 4], d.collect(&:id)
237 end
237 end
238
238
239 def test_users_by_role
239 def test_users_by_role
240 users_by_role = Project.find(1).users_by_role
240 users_by_role = Project.find(1).users_by_role
241 assert_kind_of Hash, users_by_role
241 assert_kind_of Hash, users_by_role
242 role = Role.find(1)
242 role = Role.find(1)
243 assert_kind_of Array, users_by_role[role]
243 assert_kind_of Array, users_by_role[role]
244 assert users_by_role[role].include?(User.find(2))
244 assert users_by_role[role].include?(User.find(2))
245 end
245 end
246
246
247 def test_rolled_up_trackers
247 def test_rolled_up_trackers
248 parent = Project.find(1)
248 parent = Project.find(1)
249 parent.trackers = Tracker.find([1,2])
249 parent.trackers = Tracker.find([1,2])
250 child = parent.children.find(3)
250 child = parent.children.find(3)
251
251
252 assert_equal [1, 2], parent.tracker_ids
252 assert_equal [1, 2], parent.tracker_ids
253 assert_equal [2, 3], child.trackers.collect(&:id)
253 assert_equal [2, 3], child.trackers.collect(&:id)
254
254
255 assert_kind_of Tracker, parent.rolled_up_trackers.first
255 assert_kind_of Tracker, parent.rolled_up_trackers.first
256 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
256 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
257
257
258 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
258 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
259 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
259 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
260 end
260 end
261
261
262 def test_rolled_up_trackers_should_ignore_archived_subprojects
262 def test_rolled_up_trackers_should_ignore_archived_subprojects
263 parent = Project.find(1)
263 parent = Project.find(1)
264 parent.trackers = Tracker.find([1,2])
264 parent.trackers = Tracker.find([1,2])
265 child = parent.children.find(3)
265 child = parent.children.find(3)
266 child.trackers = Tracker.find([1,3])
266 child.trackers = Tracker.find([1,3])
267 parent.children.each(&:archive)
267 parent.children.each(&:archive)
268
268
269 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
269 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
270 end
270 end
271
271
272 def test_next_identifier
272 def test_next_identifier
273 ProjectCustomField.delete_all
273 ProjectCustomField.delete_all
274 Project.create!(:name => 'last', :identifier => 'p2008040')
274 Project.create!(:name => 'last', :identifier => 'p2008040')
275 assert_equal 'p2008041', Project.next_identifier
275 assert_equal 'p2008041', Project.next_identifier
276 end
276 end
277
277
278 def test_next_identifier_first_project
278 def test_next_identifier_first_project
279 Project.delete_all
279 Project.delete_all
280 assert_nil Project.next_identifier
280 assert_nil Project.next_identifier
281 end
281 end
282
282
283
283
284 def test_enabled_module_names_should_not_recreate_enabled_modules
284 def test_enabled_module_names_should_not_recreate_enabled_modules
285 project = Project.find(1)
285 project = Project.find(1)
286 # Remove one module
286 # Remove one module
287 modules = project.enabled_modules.slice(0..-2)
287 modules = project.enabled_modules.slice(0..-2)
288 assert modules.any?
288 assert modules.any?
289 assert_difference 'EnabledModule.count', -1 do
289 assert_difference 'EnabledModule.count', -1 do
290 project.enabled_module_names = modules.collect(&:name)
290 project.enabled_module_names = modules.collect(&:name)
291 end
291 end
292 project.reload
292 project.reload
293 # Ids should be preserved
293 # Ids should be preserved
294 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
294 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
295 end
295 end
296
296
297 def test_copy_from_existing_project
297 def test_copy_from_existing_project
298 source_project = Project.find(1)
298 source_project = Project.find(1)
299 copied_project = Project.copy_from(1)
299 copied_project = Project.copy_from(1)
300
300
301 assert copied_project
301 assert copied_project
302 # Cleared attributes
302 # Cleared attributes
303 assert copied_project.id.blank?
303 assert copied_project.id.blank?
304 assert copied_project.name.blank?
304 assert copied_project.name.blank?
305 assert copied_project.identifier.blank?
305 assert copied_project.identifier.blank?
306
306
307 # Duplicated attributes
307 # Duplicated attributes
308 assert_equal source_project.description, copied_project.description
308 assert_equal source_project.description, copied_project.description
309 assert_equal source_project.enabled_modules, copied_project.enabled_modules
309 assert_equal source_project.enabled_modules, copied_project.enabled_modules
310 assert_equal source_project.trackers, copied_project.trackers
310 assert_equal source_project.trackers, copied_project.trackers
311
311
312 # Default attributes
312 # Default attributes
313 assert_equal 1, copied_project.status
313 assert_equal 1, copied_project.status
314 end
314 end
315
315
316 def test_activities_should_use_the_system_activities
317 project = Project.find(1)
318 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
319 end
320
321
322 def test_activities_should_use_the_project_specific_activities
323 project = Project.find(1)
324 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
325 assert overridden_activity.save!
326
327 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
328 end
329
330 def test_activities_should_not_include_the_inactive_project_specific_activities
331 project = Project.find(1)
332 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
333 assert overridden_activity.save!
334
335 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
336 end
337
338 def test_activities_should_not_include_project_specific_activities_from_other_projects
339 project = Project.find(1)
340 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
341 assert overridden_activity.save!
342
343 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
344 end
345
346 def test_activities_should_handle_nils
347 TimeEntryActivity.delete_all
348
349 project = Project.find(1)
350 assert project.activities.empty?
351 end
352
353 def test_activities_should_override_system_activities_with_project_activities
354 project = Project.find(1)
355 parent_activity = TimeEntryActivity.find(:first)
356 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
357 assert overridden_activity.save!
358
359 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
360 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
361 end
362
316 context "Project#copy" do
363 context "Project#copy" do
317 setup do
364 setup do
318 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
365 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
319 Project.destroy_all :identifier => "copy-test"
366 Project.destroy_all :identifier => "copy-test"
320 @source_project = Project.find(2)
367 @source_project = Project.find(2)
321 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
368 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
322 @project.trackers = @source_project.trackers
369 @project.trackers = @source_project.trackers
323 @project.enabled_modules = @source_project.enabled_modules
370 @project.enabled_modules = @source_project.enabled_modules
324 end
371 end
325
372
326 should "copy issues" do
373 should "copy issues" do
327 assert @project.valid?
374 assert @project.valid?
328 assert @project.issues.empty?
375 assert @project.issues.empty?
329 assert @project.copy(@source_project)
376 assert @project.copy(@source_project)
330
377
331 assert_equal @source_project.issues.size, @project.issues.size
378 assert_equal @source_project.issues.size, @project.issues.size
332 @project.issues.each do |issue|
379 @project.issues.each do |issue|
333 assert issue.valid?
380 assert issue.valid?
334 assert ! issue.assigned_to.blank?
381 assert ! issue.assigned_to.blank?
335 assert_equal @project, issue.project
382 assert_equal @project, issue.project
336 end
383 end
337 end
384 end
338
385
339 should "change the new issues to use the copied version" do
386 should "change the new issues to use the copied version" do
340 assigned_version = Version.generate!(:name => "Assigned Issues")
387 assigned_version = Version.generate!(:name => "Assigned Issues")
341 @source_project.versions << assigned_version
388 @source_project.versions << assigned_version
342 assert_equal 1, @source_project.versions.size
389 assert_equal 1, @source_project.versions.size
343 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
390 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
344 :subject => "change the new issues to use the copied version",
391 :subject => "change the new issues to use the copied version",
345 :tracker_id => 1,
392 :tracker_id => 1,
346 :project_id => @source_project.id)
393 :project_id => @source_project.id)
347
394
348 assert @project.copy(@source_project)
395 assert @project.copy(@source_project)
349 @project.reload
396 @project.reload
350 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
397 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
351
398
352 assert copied_issue
399 assert copied_issue
353 assert copied_issue.fixed_version
400 assert copied_issue.fixed_version
354 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
401 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
355 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
402 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
356 end
403 end
357
404
358 should "copy members" do
405 should "copy members" do
359 assert @project.valid?
406 assert @project.valid?
360 assert @project.members.empty?
407 assert @project.members.empty?
361 assert @project.copy(@source_project)
408 assert @project.copy(@source_project)
362
409
363 assert_equal @source_project.members.size, @project.members.size
410 assert_equal @source_project.members.size, @project.members.size
364 @project.members.each do |member|
411 @project.members.each do |member|
365 assert member
412 assert member
366 assert_equal @project, member.project
413 assert_equal @project, member.project
367 end
414 end
368 end
415 end
369
416
370 should "copy project specific queries" do
417 should "copy project specific queries" do
371 assert @project.valid?
418 assert @project.valid?
372 assert @project.queries.empty?
419 assert @project.queries.empty?
373 assert @project.copy(@source_project)
420 assert @project.copy(@source_project)
374
421
375 assert_equal @source_project.queries.size, @project.queries.size
422 assert_equal @source_project.queries.size, @project.queries.size
376 @project.queries.each do |query|
423 @project.queries.each do |query|
377 assert query
424 assert query
378 assert_equal @project, query.project
425 assert_equal @project, query.project
379 end
426 end
380 end
427 end
381
428
382 should "copy versions" do
429 should "copy versions" do
383 @source_project.versions << Version.generate!
430 @source_project.versions << Version.generate!
384 @source_project.versions << Version.generate!
431 @source_project.versions << Version.generate!
385
432
386 assert @project.versions.empty?
433 assert @project.versions.empty?
387 assert @project.copy(@source_project)
434 assert @project.copy(@source_project)
388
435
389 assert_equal @source_project.versions.size, @project.versions.size
436 assert_equal @source_project.versions.size, @project.versions.size
390 @project.versions.each do |version|
437 @project.versions.each do |version|
391 assert version
438 assert version
392 assert_equal @project, version.project
439 assert_equal @project, version.project
393 end
440 end
394 end
441 end
395
442
396 should "copy wiki" do
443 should "copy wiki" do
397 assert @project.copy(@source_project)
444 assert @project.copy(@source_project)
398
445
399 assert @project.wiki
446 assert @project.wiki
400 assert_not_equal @source_project.wiki, @project.wiki
447 assert_not_equal @source_project.wiki, @project.wiki
401 assert_equal "Start page", @project.wiki.start_page
448 assert_equal "Start page", @project.wiki.start_page
402 end
449 end
403
450
404 should "copy wiki pages and content" do
451 should "copy wiki pages and content" do
405 assert @project.copy(@source_project)
452 assert @project.copy(@source_project)
406
453
407 assert @project.wiki
454 assert @project.wiki
408 assert_equal 1, @project.wiki.pages.length
455 assert_equal 1, @project.wiki.pages.length
409
456
410 @project.wiki.pages.each do |wiki_page|
457 @project.wiki.pages.each do |wiki_page|
411 assert wiki_page.content
458 assert wiki_page.content
412 assert !@source_project.wiki.pages.include?(wiki_page)
459 assert !@source_project.wiki.pages.include?(wiki_page)
413 end
460 end
414 end
461 end
415
462
416 should "copy custom fields"
463 should "copy custom fields"
417
464
418 should "copy issue categories" do
465 should "copy issue categories" do
419 assert @project.copy(@source_project)
466 assert @project.copy(@source_project)
420
467
421 assert_equal 2, @project.issue_categories.size
468 assert_equal 2, @project.issue_categories.size
422 @project.issue_categories.each do |issue_category|
469 @project.issue_categories.each do |issue_category|
423 assert !@source_project.issue_categories.include?(issue_category)
470 assert !@source_project.issue_categories.include?(issue_category)
424 end
471 end
425 end
472 end
426
473
427 should "change the new issues to use the copied issue categories" do
474 should "change the new issues to use the copied issue categories" do
428 issue = Issue.find(4)
475 issue = Issue.find(4)
429 issue.update_attribute(:category_id, 3)
476 issue.update_attribute(:category_id, 3)
430
477
431 assert @project.copy(@source_project)
478 assert @project.copy(@source_project)
432
479
433 @project.issues.each do |issue|
480 @project.issues.each do |issue|
434 assert issue.category
481 assert issue.category
435 assert_equal "Stock management", issue.category.name # Same name
482 assert_equal "Stock management", issue.category.name # Same name
436 assert_not_equal IssueCategory.find(3), issue.category # Different record
483 assert_not_equal IssueCategory.find(3), issue.category # Different record
437 end
484 end
438 end
485 end
439
486
440 should "copy issue relations"
487 should "copy issue relations"
441 should "link issue relations if cross project issue relations are valid"
488 should "link issue relations if cross project issue relations are valid"
442
489
443 end
490 end
444
491
445 end
492 end
General Comments 0
You need to be logged in to leave comments. Login now