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