##// END OF EJS Templates
Merged r7983 and r7984 from trunk....
Jean-Philippe Lang -
r7880:fa894328bb94
parent child
Show More
@@ -1,234 +1,239
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Version < ActiveRecord::Base
19 19 after_update :update_issues_from_sharing_change
20 20 belongs_to :project
21 21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 22 acts_as_customizable
23 23 acts_as_attachable :view_permission => :view_files,
24 24 :delete_permission => :manage_files
25 25
26 26 VERSION_STATUSES = %w(open locked closed)
27 27 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28 28
29 29 validates_presence_of :name
30 30 validates_uniqueness_of :name, :scope => [:project_id]
31 31 validates_length_of :name, :maximum => 60
32 32 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 33 validates_inclusion_of :status, :in => VERSION_STATUSES
34 34 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35 35
36 36 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
37 37 named_scope :open, :conditions => {:status => 'open'}
38 38 named_scope :visible, lambda {|*args| { :include => :project,
39 39 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40 40
41 41 # Returns true if +user+ or current user is allowed to view the version
42 42 def visible?(user=User.current)
43 43 user.allowed_to?(:view_issues, self.project)
44 44 end
45 45
46 # Version files have same visibility as project files
47 def attachments_visible?(*args)
48 project.present? && project.attachments_visible?(*args)
49 end
50
46 51 def start_date
47 52 @start_date ||= fixed_issues.minimum('start_date')
48 53 end
49 54
50 55 def due_date
51 56 effective_date
52 57 end
53 58
54 59 # Returns the total estimated time for this version
55 60 # (sum of leaves estimated_hours)
56 61 def estimated_hours
57 62 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
58 63 end
59 64
60 65 # Returns the total reported time for this version
61 66 def spent_hours
62 67 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
63 68 end
64 69
65 70 def closed?
66 71 status == 'closed'
67 72 end
68 73
69 74 def open?
70 75 status == 'open'
71 76 end
72 77
73 78 # Returns true if the version is completed: due date reached and no open issues
74 79 def completed?
75 80 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
76 81 end
77 82
78 83 def behind_schedule?
79 84 if completed_pourcent == 100
80 85 return false
81 86 elsif due_date && start_date
82 87 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
83 88 return done_date <= Date.today
84 89 else
85 90 false # No issues so it's not late
86 91 end
87 92 end
88 93
89 94 # Returns the completion percentage of this version based on the amount of open/closed issues
90 95 # and the time spent on the open issues.
91 96 def completed_pourcent
92 97 if issues_count == 0
93 98 0
94 99 elsif open_issues_count == 0
95 100 100
96 101 else
97 102 issues_progress(false) + issues_progress(true)
98 103 end
99 104 end
100 105
101 106 # Returns the percentage of issues that have been marked as 'closed'.
102 107 def closed_pourcent
103 108 if issues_count == 0
104 109 0
105 110 else
106 111 issues_progress(false)
107 112 end
108 113 end
109 114
110 115 # Returns true if the version is overdue: due date reached and some open issues
111 116 def overdue?
112 117 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
113 118 end
114 119
115 120 # Returns assigned issues count
116 121 def issues_count
117 122 @issue_count ||= fixed_issues.count
118 123 end
119 124
120 125 # Returns the total amount of open issues for this version.
121 126 def open_issues_count
122 127 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
123 128 end
124 129
125 130 # Returns the total amount of closed issues for this version.
126 131 def closed_issues_count
127 132 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
128 133 end
129 134
130 135 def wiki_page
131 136 if project.wiki && !wiki_page_title.blank?
132 137 @wiki_page ||= project.wiki.find_page(wiki_page_title)
133 138 end
134 139 @wiki_page
135 140 end
136 141
137 142 def to_s; name end
138 143
139 144 def to_s_with_project
140 145 "#{project} - #{name}"
141 146 end
142 147
143 148 # Versions are sorted by effective_date and "Project Name - Version name"
144 149 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
145 150 def <=>(version)
146 151 if self.effective_date
147 152 if version.effective_date
148 153 if self.effective_date == version.effective_date
149 154 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
150 155 else
151 156 self.effective_date <=> version.effective_date
152 157 end
153 158 else
154 159 -1
155 160 end
156 161 else
157 162 if version.effective_date
158 163 1
159 164 else
160 165 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
161 166 end
162 167 end
163 168 end
164 169
165 170 # Returns the sharings that +user+ can set the version to
166 171 def allowed_sharings(user = User.current)
167 172 VERSION_SHARINGS.select do |s|
168 173 if sharing == s
169 174 true
170 175 else
171 176 case s
172 177 when 'system'
173 178 # Only admin users can set a systemwide sharing
174 179 user.admin?
175 180 when 'hierarchy', 'tree'
176 181 # Only users allowed to manage versions of the root project can
177 182 # set sharing to hierarchy or tree
178 183 project.nil? || user.allowed_to?(:manage_versions, project.root)
179 184 else
180 185 true
181 186 end
182 187 end
183 188 end
184 189 end
185 190
186 191 private
187 192
188 193 # Update the issue's fixed versions. Used if a version's sharing changes.
189 194 def update_issues_from_sharing_change
190 195 if sharing_changed?
191 196 if VERSION_SHARINGS.index(sharing_was).nil? ||
192 197 VERSION_SHARINGS.index(sharing).nil? ||
193 198 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
194 199 Issue.update_versions_from_sharing_change self
195 200 end
196 201 end
197 202 end
198 203
199 204 # Returns the average estimated time of assigned issues
200 205 # or 1 if no issue has an estimated time
201 206 # Used to weigth unestimated issues in progress calculation
202 207 def estimated_average
203 208 if @estimated_average.nil?
204 209 average = fixed_issues.average(:estimated_hours).to_f
205 210 if average == 0
206 211 average = 1
207 212 end
208 213 @estimated_average = average
209 214 end
210 215 @estimated_average
211 216 end
212 217
213 218 # Returns the total progress of open or closed issues. The returned percentage takes into account
214 219 # the amount of estimated time set for this version.
215 220 #
216 221 # Examples:
217 222 # issues_progress(true) => returns the progress percentage for open issues.
218 223 # issues_progress(false) => returns the progress percentage for closed issues.
219 224 def issues_progress(open)
220 225 @issues_progress ||= {}
221 226 @issues_progress[open] ||= begin
222 227 progress = 0
223 228 if issues_count > 0
224 229 ratio = open ? 'done_ratio' : 100
225 230
226 231 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
227 232 :include => :status,
228 233 :conditions => ["is_closed = ?", !open]).to_f
229 234 progress = done / (estimated_average * issues_count)
230 235 end
231 236 progress
232 237 end
233 238 end
234 239 end
@@ -1,184 +1,184
1 1 ---
2 2 attachments_001:
3 3 created_on: 2006-07-19 21:07:27 +02:00
4 4 downloads: 0
5 5 content_type: text/plain
6 6 disk_filename: 060719210727_error281.txt
7 7 container_id: 3
8 8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 9 id: 1
10 10 container_type: Issue
11 11 filesize: 28
12 12 filename: error281.txt
13 13 author_id: 2
14 14 attachments_002:
15 15 created_on: 2007-01-27 15:08:27 +01:00
16 16 downloads: 0
17 17 content_type: text/plain
18 18 disk_filename: 060719210727_document.txt
19 19 container_id: 1
20 20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 21 id: 2
22 22 container_type: Document
23 23 filesize: 28
24 24 filename: document.txt
25 25 author_id: 2
26 26 attachments_003:
27 27 created_on: 2006-07-19 21:07:27 +02:00
28 28 downloads: 0
29 29 content_type: image/gif
30 30 disk_filename: 060719210727_logo.gif
31 31 container_id: 4
32 32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 33 id: 3
34 34 container_type: WikiPage
35 35 filesize: 280
36 36 filename: logo.gif
37 37 description: This is a logo
38 38 author_id: 2
39 39 attachments_004:
40 40 created_on: 2006-07-19 21:07:27 +02:00
41 41 container_type: Issue
42 42 container_id: 3
43 43 downloads: 0
44 44 disk_filename: 060719210727_source.rb
45 45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 46 id: 4
47 47 filesize: 153
48 48 filename: source.rb
49 49 author_id: 2
50 50 description: This is a Ruby source file
51 51 content_type: application/x-ruby
52 52 attachments_005:
53 53 created_on: 2006-07-19 21:07:27 +02:00
54 54 container_type: Issue
55 55 container_id: 3
56 56 downloads: 0
57 57 disk_filename: 060719210727_changeset_iso8859-1.diff
58 58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 59 id: 5
60 60 filesize: 687
61 61 filename: changeset_iso8859-1.diff
62 62 author_id: 2
63 63 content_type: text/x-diff
64 64 attachments_006:
65 65 created_on: 2006-07-19 21:07:27 +02:00
66 66 container_type: Issue
67 67 container_id: 3
68 68 downloads: 0
69 69 disk_filename: 060719210727_archive.zip
70 70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 71 id: 6
72 72 filesize: 157
73 73 filename: archive.zip
74 74 author_id: 2
75 75 content_type: application/octet-stream
76 76 attachments_007:
77 77 created_on: 2006-07-19 21:07:27 +02:00
78 78 container_type: Issue
79 79 container_id: 4
80 80 downloads: 0
81 81 disk_filename: 060719210727_archive.zip
82 82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 83 id: 7
84 84 filesize: 157
85 85 filename: archive.zip
86 86 author_id: 1
87 87 content_type: application/octet-stream
88 88 attachments_008:
89 89 created_on: 2006-07-19 21:07:27 +02:00
90 90 container_type: Project
91 91 container_id: 1
92 92 downloads: 0
93 93 disk_filename: 060719210727_project_file.zip
94 94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 95 id: 8
96 96 filesize: 320
97 97 filename: project_file.zip
98 98 author_id: 2
99 99 content_type: application/octet-stream
100 100 attachments_009:
101 101 created_on: 2006-07-19 21:07:27 +02:00
102 102 container_type: Version
103 103 container_id: 1
104 104 downloads: 0
105 disk_filename: 060719210727_version_file.zip
105 disk_filename: 060719210727_archive.zip
106 106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 107 id: 9
108 108 filesize: 452
109 109 filename: version_file.zip
110 110 author_id: 2
111 111 content_type: application/octet-stream
112 112 attachments_010:
113 113 created_on: 2006-07-19 21:07:27 +02:00
114 114 container_type: Issue
115 115 container_id: 2
116 116 downloads: 0
117 117 disk_filename: 060719210727_picture.jpg
118 118 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
119 119 id: 10
120 120 filesize: 452
121 121 filename: picture.jpg
122 122 author_id: 2
123 123 content_type: image/jpeg
124 124 attachments_011:
125 125 created_on: 2007-02-12 15:08:27 +01:00
126 126 container_type: Document
127 127 container_id: 1
128 128 downloads: 0
129 129 disk_filename: 060719210727_picture.jpg
130 130 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
131 131 id: 11
132 132 filesize: 452
133 133 filename: picture.jpg
134 134 author_id: 2
135 135 content_type: image/jpeg
136 136 attachments_012:
137 137 created_on: 2006-07-19 21:07:27 +02:00
138 138 container_type: Version
139 139 container_id: 1
140 140 downloads: 0
141 141 disk_filename: 060719210727_version_file.zip
142 142 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
143 143 id: 12
144 144 filesize: 452
145 145 filename: version_file.zip
146 146 author_id: 2
147 147 content_type: application/octet-stream
148 148 attachments_013:
149 149 created_on: 2006-07-19 21:07:27 +02:00
150 150 container_type: Message
151 151 container_id: 1
152 152 downloads: 0
153 153 disk_filename: 060719210727_foo.zip
154 154 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
155 155 id: 13
156 156 filesize: 452
157 157 filename: foo.zip
158 158 author_id: 2
159 159 content_type: application/octet-stream
160 160 attachments_014:
161 161 created_on: 2006-07-19 21:07:27 +02:00
162 162 container_type: Issue
163 163 container_id: 3
164 164 downloads: 0
165 165 disk_filename: 060719210727_changeset_utf8.diff
166 166 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
167 167 id: 14
168 168 filesize: 687
169 169 filename: changeset_utf8.diff
170 170 author_id: 2
171 171 content_type: text/x-diff
172 172 attachments_015:
173 173 id: 15
174 174 created_on: 2010-07-19 21:07:27 +02:00
175 175 container_type: Issue
176 176 container_id: 14
177 177 downloads: 0
178 178 disk_filename: 060719210727_changeset_utf8.diff
179 179 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
180 180 filesize: 687
181 181 filename: private.diff
182 182 author_id: 2
183 183 content_type: text/x-diff
184 184 description: attachement of a private issue
@@ -1,170 +1,183
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21 require 'attachments_controller'
22 22
23 23 # Re-raise errors caught by the controller.
24 24 class AttachmentsController; def rescue_action(e) raise e end; end
25 25
26 26
27 27 class AttachmentsControllerTest < ActionController::TestCase
28 28 fixtures :users, :projects, :roles, :members, :member_roles, :enabled_modules, :issues, :trackers, :attachments,
29 29 :versions, :wiki_pages, :wikis, :documents
30 30
31 31 def setup
32 32 @controller = AttachmentsController.new
33 33 @request = ActionController::TestRequest.new
34 34 @response = ActionController::TestResponse.new
35 35 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
36 36 User.current = nil
37 37 end
38 38
39 39 def test_show_diff
40 40 get :show, :id => 14 # 060719210727_changeset_utf8.diff
41 41 assert_response :success
42 42 assert_template 'diff'
43 43 assert_equal 'text/html', @response.content_type
44 44
45 45 assert_tag 'th',
46 46 :attributes => {:class => /filename/},
47 47 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
48 48 assert_tag 'td',
49 49 :attributes => {:class => /line-code/},
50 50 :content => /Demande créée avec succès/
51 51 end
52 52
53 53 def test_show_diff_should_strip_non_utf8_content
54 54 get :show, :id => 5 # 060719210727_changeset_iso8859-1.diff
55 55 assert_response :success
56 56 assert_template 'diff'
57 57 assert_equal 'text/html', @response.content_type
58 58
59 59 assert_tag 'th',
60 60 :attributes => {:class => /filename/},
61 61 :content => /issues_controller.rb\t\(rvision 1484\)/
62 62 assert_tag 'td',
63 63 :attributes => {:class => /line-code/},
64 64 :content => /Demande cre avec succs/
65 65 end
66 66
67 67 def test_show_text_file
68 68 get :show, :id => 4
69 69 assert_response :success
70 70 assert_template 'file'
71 71 assert_equal 'text/html', @response.content_type
72 72 end
73 73
74 74 def test_show_text_file_should_send_if_too_big
75 75 Setting.file_max_size_displayed = 512
76 76 Attachment.find(4).update_attribute :filesize, 754.kilobyte
77 77
78 78 get :show, :id => 4
79 79 assert_response :success
80 80 assert_equal 'application/x-ruby', @response.content_type
81 81 end
82 82
83 83 def test_show_other
84 84 get :show, :id => 6
85 85 assert_response :success
86 86 assert_equal 'application/octet-stream', @response.content_type
87 87 end
88 88
89 89 def test_show_file_from_private_issue_without_permission
90 90 get :show, :id => 15
91 91 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
92 92 end
93 93
94 94 def test_show_file_from_private_issue_with_permission
95 95 @request.session[:user_id] = 2
96 96 get :show, :id => 15
97 97 assert_response :success
98 98 assert_tag 'h2', :content => /private.diff/
99 99 end
100 100
101 101 def test_download_text_file
102 102 get :download, :id => 4
103 103 assert_response :success
104 104 assert_equal 'application/x-ruby', @response.content_type
105 105 end
106 106
107 def test_download_version_file_with_issue_tracking_disabled
108 Project.find(1).disable_module! :issue_tracking
109 get :download, :id => 9
110 assert_response :success
111 end
112
107 113 def test_download_should_assign_content_type_if_blank
108 114 Attachment.find(4).update_attribute(:content_type, '')
109 115
110 116 get :download, :id => 4
111 117 assert_response :success
112 118 assert_equal 'text/x-ruby', @response.content_type
113 119 end
114 120
115 121 def test_download_missing_file
116 122 get :download, :id => 2
117 123 assert_response 404
118 124 end
119 125
120 126 def test_anonymous_on_private_private
121 127 get :download, :id => 7
122 128 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
123 129 end
124 130
125 131 def test_destroy_issue_attachment
132 set_tmp_attachments_directory
126 133 issue = Issue.find(3)
127 134 @request.session[:user_id] = 2
128 135
129 136 assert_difference 'issue.attachments.count', -1 do
130 137 post :destroy, :id => 1
131 138 end
132 139 # no referrer
133 140 assert_redirected_to '/projects/ecookbook'
134 141 assert_nil Attachment.find_by_id(1)
135 142 j = issue.journals.find(:first, :order => 'created_on DESC')
136 143 assert_equal 'attachment', j.details.first.property
137 144 assert_equal '1', j.details.first.prop_key
138 145 assert_equal 'error281.txt', j.details.first.old_value
139 146 end
140 147
141 148 def test_destroy_wiki_page_attachment
149 set_tmp_attachments_directory
142 150 @request.session[:user_id] = 2
143 151 assert_difference 'Attachment.count', -1 do
144 152 post :destroy, :id => 3
145 153 assert_response 302
146 154 end
147 155 end
148 156
149 157 def test_destroy_project_attachment
158 set_tmp_attachments_directory
150 159 @request.session[:user_id] = 2
151 160 assert_difference 'Attachment.count', -1 do
152 161 post :destroy, :id => 8
153 162 assert_response 302
154 163 end
155 164 end
156 165
157 166 def test_destroy_version_attachment
167 set_tmp_attachments_directory
158 168 @request.session[:user_id] = 2
159 169 assert_difference 'Attachment.count', -1 do
160 170 post :destroy, :id => 9
161 171 assert_response 302
162 172 end
163 173 end
164 174
165 175 def test_destroy_without_permission
166 post :destroy, :id => 3
167 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdestroy%2F3'
176 set_tmp_attachments_directory
177 assert_no_difference 'Attachment.count' do
178 delete :destroy, :id => 3
179 end
180 assert_response 302
168 181 assert Attachment.find_by_id(3)
169 182 end
170 183 end
General Comments 0
You need to be logged in to leave comments. Login now