##// END OF EJS Templates
Upgrade to Rails 4.2.0 (#14534)....
Jean-Philippe Lang -
r13510:d85f73a30d48
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,104 +1,105
1 1 source 'https://rubygems.org'
2 2
3 gem "rails", "4.1.8"
3 gem "rails", "4.2.0"
4 4 gem "jquery-rails", "~> 3.1.1"
5 5 gem "coderay", "~> 1.1.0"
6 6 gem "builder", ">= 3.0.4"
7 7 gem "request_store", "1.0.5"
8 8 gem "mime-types"
9 9 gem "protected_attributes"
10 10 gem "actionpack-action_caching"
11 11 gem "actionpack-xml_parser"
12 12
13 13 # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
14 14 gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin, :jruby]
15 15 gem "rbpdf", "~> 1.18.4"
16 16
17 17 # Optional gem for LDAP authentication
18 18 group :ldap do
19 19 gem "net-ldap", "~> 0.3.1"
20 20 end
21 21
22 22 # Optional gem for OpenID authentication
23 23 group :openid do
24 24 gem "ruby-openid", "~> 2.3.0", :require => "openid"
25 25 gem "rack-openid"
26 26 end
27 27
28 28 platforms :mri, :mingw, :x64_mingw do
29 29 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
30 30 group :rmagick do
31 31 gem "rmagick", ">= 2.0.0"
32 32 end
33 33
34 34 # Optional Markdown support, not for JRuby
35 35 group :markdown do
36 36 gem "redcarpet", "~> 3.1.2"
37 37 end
38 38 end
39 39
40 40 platforms :jruby do
41 41 # jruby-openssl is bundled with JRuby 1.7.0
42 42 gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0'
43 43 gem "activerecord-jdbc-adapter", "~> 1.3.2"
44 44 end
45 45
46 46 # Include database gems for the adapters found in the database
47 47 # configuration file
48 48 require 'erb'
49 49 require 'yaml'
50 50 database_file = File.join(File.dirname(__FILE__), "config/database.yml")
51 51 if File.exist?(database_file)
52 52 database_config = YAML::load(ERB.new(IO.read(database_file)).result)
53 53 adapters = database_config.values.map {|c| c['adapter']}.compact.uniq
54 54 if adapters.any?
55 55 adapters.each do |adapter|
56 56 case adapter
57 57 when 'mysql2'
58 58 gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw, :x64_mingw]
59 59 gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
60 60 when 'mysql'
61 61 gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
62 62 when /postgresql/
63 63 gem "pg", "~> 0.17.1", :platforms => [:mri, :mingw, :x64_mingw]
64 64 gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby
65 65 when /sqlite3/
66 66 gem "sqlite3", :platforms => [:mri, :mingw, :x64_mingw]
67 67 gem "activerecord-jdbcsqlite3-adapter", "1.3.11", :platforms => :jruby
68 68 when /sqlserver/
69 69 gem "tiny_tds", "~> 0.6.2", :platforms => [:mri, :mingw, :x64_mingw]
70 70 gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw, :x64_mingw]
71 71 else
72 72 warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems")
73 73 end
74 74 end
75 75 else
76 76 warn("No adapter found in config/database.yml, please configure it first")
77 77 end
78 78 else
79 79 warn("Please configure your config/database.yml first")
80 80 end
81 81
82 82 group :development do
83 83 gem "rdoc", ">= 2.4.2"
84 84 gem "yard"
85 85 end
86 86
87 87 group :test do
88 88 gem "minitest"
89 gem "mocha", "~> 1.0.0", :require => 'mocha/api'
89 gem "rails-dom-testing"
90 gem "mocha", "~> 1.0.0"
90 91 gem "simplecov", "~> 0.9.1", :require => false
91 92 # For running UI tests
92 93 gem "capybara"
93 94 gem "selenium-webdriver"
94 95 end
95 96
96 97 local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
97 98 if File.exists?(local_gemfile)
98 99 eval_gemfile local_gemfile
99 100 end
100 101
101 102 # Load plugins' Gemfiles
102 103 Dir.glob File.expand_path("../plugins/*/{Gemfile,PluginGemfile}", __FILE__) do |file|
103 104 eval_gemfile file
104 105 end
@@ -1,1312 +1,1316
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_path(issue, :only_path => only_path),
83 s = link_to(text, issue_url(issue, :only_path => only_path),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 97 html_options = options.slice!(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
98 99 url = send(route_method, attachment, attachment.filename, options)
99 100 link_to text, url, html_options
100 101 end
101 102
102 103 # Generates a link to a SCM revision
103 104 # Options:
104 105 # * :text - Link text (default to the formatted revision)
105 106 def link_to_revision(revision, repository, options={})
106 107 if repository.is_a?(Project)
107 108 repository = repository.repository
108 109 end
109 110 text = options.delete(:text) || format_revision(revision)
110 111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 112 link_to(
112 113 h(text),
113 114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 115 :title => l(:label_revision_id, format_revision(revision)),
115 116 :accesskey => options[:accesskey]
116 117 )
117 118 end
118 119
119 120 # Generates a link to a message
120 121 def link_to_message(message, options={}, html_options = nil)
121 122 link_to(
122 123 message.subject.truncate(60),
123 board_message_path(message.board_id, message.parent_id || message.id, {
124 board_message_url(message.board_id, message.parent_id || message.id, {
124 125 :r => (message.parent_id && message.id),
125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 :only_path => true
126 128 }.merge(options)),
127 129 html_options
128 130 )
129 131 end
130 132
131 133 # Generates a link to a project if active
132 134 # Examples:
133 135 #
134 136 # link_to_project(project) # => link to the specified project overview
135 137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
136 138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
137 139 #
138 140 def link_to_project(project, options={}, html_options = nil)
139 141 if project.archived?
140 142 h(project.name)
141 143 else
142 link_to project.name, project_path(project, options), html_options
144 link_to project.name,
145 project_url(project, {:only_path => true}.merge(options)),
146 html_options
143 147 end
144 148 end
145 149
146 150 # Generates a link to a project settings if active
147 151 def link_to_project_settings(project, options={}, html_options=nil)
148 152 if project.active?
149 153 link_to project.name, settings_project_path(project, options), html_options
150 154 elsif project.archived?
151 155 h(project.name)
152 156 else
153 157 link_to project.name, project_path(project, options), html_options
154 158 end
155 159 end
156 160
157 161 # Generates a link to a version
158 162 def link_to_version(version, options = {})
159 163 return '' unless version && version.is_a?(Version)
160 164 options = {:title => format_date(version.effective_date)}.merge(options)
161 165 link_to_if version.visible?, format_version_name(version), version_path(version), options
162 166 end
163 167
164 168 # Helper that formats object for html or text rendering
165 169 def format_object(object, html=true, &block)
166 170 if block_given?
167 171 object = yield object
168 172 end
169 173 case object.class.name
170 174 when 'Array'
171 175 object.map {|o| format_object(o, html)}.join(', ').html_safe
172 176 when 'Time'
173 177 format_time(object)
174 178 when 'Date'
175 179 format_date(object)
176 180 when 'Fixnum'
177 181 object.to_s
178 182 when 'Float'
179 183 sprintf "%.2f", object
180 184 when 'User'
181 185 html ? link_to_user(object) : object.to_s
182 186 when 'Project'
183 187 html ? link_to_project(object) : object.to_s
184 188 when 'Version'
185 189 html ? link_to_version(object) : object.to_s
186 190 when 'TrueClass'
187 191 l(:general_text_Yes)
188 192 when 'FalseClass'
189 193 l(:general_text_No)
190 194 when 'Issue'
191 195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
192 196 when 'CustomValue', 'CustomFieldValue'
193 197 if object.custom_field
194 198 f = object.custom_field.format.formatted_custom_value(self, object, html)
195 199 if f.nil? || f.is_a?(String)
196 200 f
197 201 else
198 202 format_object(f, html, &block)
199 203 end
200 204 else
201 205 object.value.to_s
202 206 end
203 207 else
204 208 html ? h(object) : object.to_s
205 209 end
206 210 end
207 211
208 212 def wiki_page_path(page, options={})
209 213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
210 214 end
211 215
212 216 def thumbnail_tag(attachment)
213 217 link_to image_tag(thumbnail_path(attachment)),
214 218 named_attachment_path(attachment, attachment.filename),
215 219 :title => attachment.filename
216 220 end
217 221
218 222 def toggle_link(name, id, options={})
219 223 onclick = "$('##{id}').toggle(); "
220 224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
221 225 onclick << "return false;"
222 226 link_to(name, "#", :onclick => onclick)
223 227 end
224 228
225 229 def format_activity_title(text)
226 230 h(truncate_single_line_raw(text, 100))
227 231 end
228 232
229 233 def format_activity_day(date)
230 234 date == User.current.today ? l(:label_today).titleize : format_date(date)
231 235 end
232 236
233 237 def format_activity_description(text)
234 238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
235 239 ).gsub(/[\r\n]+/, "<br />").html_safe
236 240 end
237 241
238 242 def format_version_name(version)
239 243 if !version.shared? || version.project == @project
240 244 h(version)
241 245 else
242 246 h("#{version.project} - #{version}")
243 247 end
244 248 end
245 249
246 250 def due_date_distance_in_words(date)
247 251 if date
248 252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
249 253 end
250 254 end
251 255
252 256 # Renders a tree of projects as a nested set of unordered lists
253 257 # The given collection may be a subset of the whole project tree
254 258 # (eg. some intermediate nodes are private and can not be seen)
255 259 def render_project_nested_lists(projects, &block)
256 260 s = ''
257 261 if projects.any?
258 262 ancestors = []
259 263 original_project = @project
260 264 projects.sort_by(&:lft).each do |project|
261 265 # set the project environment to please macros.
262 266 @project = project
263 267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
264 268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
265 269 else
266 270 ancestors.pop
267 271 s << "</li>"
268 272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
269 273 ancestors.pop
270 274 s << "</ul></li>\n"
271 275 end
272 276 end
273 277 classes = (ancestors.empty? ? 'root' : 'child')
274 278 s << "<li class='#{classes}'><div class='#{classes}'>"
275 279 s << h(block_given? ? capture(project, &block) : project.name)
276 280 s << "</div>\n"
277 281 ancestors << project
278 282 end
279 283 s << ("</li></ul>\n" * ancestors.size)
280 284 @project = original_project
281 285 end
282 286 s.html_safe
283 287 end
284 288
285 289 def render_page_hierarchy(pages, node=nil, options={})
286 290 content = ''
287 291 if pages[node]
288 292 content << "<ul class=\"pages-hierarchy\">\n"
289 293 pages[node].each do |page|
290 294 content << "<li>"
291 295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
292 296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
293 297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
294 298 content << "</li>\n"
295 299 end
296 300 content << "</ul>\n"
297 301 end
298 302 content.html_safe
299 303 end
300 304
301 305 # Renders flash messages
302 306 def render_flash_messages
303 307 s = ''
304 308 flash.each do |k,v|
305 309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
306 310 end
307 311 s.html_safe
308 312 end
309 313
310 314 # Renders tabs and their content
311 315 def render_tabs(tabs, selected=params[:tab])
312 316 if tabs.any?
313 317 unless tabs.detect {|tab| tab[:name] == selected}
314 318 selected = nil
315 319 end
316 320 selected ||= tabs.first[:name]
317 321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
318 322 else
319 323 content_tag 'p', l(:label_no_data), :class => "nodata"
320 324 end
321 325 end
322 326
323 327 # Renders the project quick-jump box
324 328 def render_project_jump_box
325 329 return unless User.current.logged?
326 330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
327 331 if projects.any?
328 332 options =
329 333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
330 334 '<option value="" disabled="disabled">---</option>').html_safe
331 335
332 336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
333 337 { :value => project_path(:id => p, :jump => current_menu_item) }
334 338 end
335 339
336 340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
337 341 end
338 342 end
339 343
340 344 def project_tree_options_for_select(projects, options = {})
341 345 s = ''.html_safe
342 346 if options[:include_blank]
343 347 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
344 348 end
345 349 project_tree(projects) do |project, level|
346 350 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
347 351 tag_options = {:value => project.id}
348 352 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
349 353 tag_options[:selected] = 'selected'
350 354 else
351 355 tag_options[:selected] = nil
352 356 end
353 357 tag_options.merge!(yield(project)) if block_given?
354 358 s << content_tag('option', name_prefix + h(project), tag_options)
355 359 end
356 360 s.html_safe
357 361 end
358 362
359 363 # Yields the given block for each project with its level in the tree
360 364 #
361 365 # Wrapper for Project#project_tree
362 366 def project_tree(projects, &block)
363 367 Project.project_tree(projects, &block)
364 368 end
365 369
366 370 def principals_check_box_tags(name, principals)
367 371 s = ''
368 372 principals.each do |principal|
369 373 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
370 374 end
371 375 s.html_safe
372 376 end
373 377
374 378 # Returns a string for users/groups option tags
375 379 def principals_options_for_select(collection, selected=nil)
376 380 s = ''
377 381 if collection.include?(User.current)
378 382 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
379 383 end
380 384 groups = ''
381 385 collection.sort.each do |element|
382 386 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
383 387 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
384 388 end
385 389 unless groups.empty?
386 390 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
387 391 end
388 392 s.html_safe
389 393 end
390 394
391 395 def option_tag(name, text, value, selected=nil, options={})
392 396 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
393 397 end
394 398
395 399 def truncate_single_line_raw(string, length)
396 400 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
397 401 end
398 402
399 403 # Truncates at line break after 250 characters or options[:length]
400 404 def truncate_lines(string, options={})
401 405 length = options[:length] || 250
402 406 if string.to_s =~ /\A(.{#{length}}.*?)$/m
403 407 "#{$1}..."
404 408 else
405 409 string
406 410 end
407 411 end
408 412
409 413 def anchor(text)
410 414 text.to_s.gsub(' ', '_')
411 415 end
412 416
413 417 def html_hours(text)
414 418 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
415 419 end
416 420
417 421 def authoring(created, author, options={})
418 422 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
419 423 end
420 424
421 425 def time_tag(time)
422 426 text = distance_of_time_in_words(Time.now, time)
423 427 if @project
424 428 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
425 429 else
426 430 content_tag('abbr', text, :title => format_time(time))
427 431 end
428 432 end
429 433
430 434 def syntax_highlight_lines(name, content)
431 435 lines = []
432 436 syntax_highlight(name, content).each_line { |line| lines << line }
433 437 lines
434 438 end
435 439
436 440 def syntax_highlight(name, content)
437 441 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
438 442 end
439 443
440 444 def to_path_param(path)
441 445 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
442 446 str.blank? ? nil : str
443 447 end
444 448
445 449 def reorder_links(name, url, method = :post)
446 450 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
447 451 url.merge({"#{name}[move_to]" => 'highest'}),
448 452 :method => method, :title => l(:label_sort_highest)) +
449 453 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
450 454 url.merge({"#{name}[move_to]" => 'higher'}),
451 455 :method => method, :title => l(:label_sort_higher)) +
452 456 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
453 457 url.merge({"#{name}[move_to]" => 'lower'}),
454 458 :method => method, :title => l(:label_sort_lower)) +
455 459 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
456 460 url.merge({"#{name}[move_to]" => 'lowest'}),
457 461 :method => method, :title => l(:label_sort_lowest))
458 462 end
459 463
460 464 def breadcrumb(*args)
461 465 elements = args.flatten
462 466 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
463 467 end
464 468
465 469 def other_formats_links(&block)
466 470 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
467 471 yield Redmine::Views::OtherFormatsBuilder.new(self)
468 472 concat('</p>'.html_safe)
469 473 end
470 474
471 475 def page_header_title
472 476 if @project.nil? || @project.new_record?
473 477 h(Setting.app_title)
474 478 else
475 479 b = []
476 480 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
477 481 if ancestors.any?
478 482 root = ancestors.shift
479 483 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
480 484 if ancestors.size > 2
481 485 b << "\xe2\x80\xa6"
482 486 ancestors = ancestors[-2, 2]
483 487 end
484 488 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
485 489 end
486 490 b << h(@project)
487 491 b.join(" \xc2\xbb ").html_safe
488 492 end
489 493 end
490 494
491 495 # Returns a h2 tag and sets the html title with the given arguments
492 496 def title(*args)
493 497 strings = args.map do |arg|
494 498 if arg.is_a?(Array) && arg.size >= 2
495 499 link_to(*arg)
496 500 else
497 501 h(arg.to_s)
498 502 end
499 503 end
500 504 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
501 505 content_tag('h2', strings.join(' &#187; ').html_safe)
502 506 end
503 507
504 508 # Sets the html title
505 509 # Returns the html title when called without arguments
506 510 # Current project name and app_title and automatically appended
507 511 # Exemples:
508 512 # html_title 'Foo', 'Bar'
509 513 # html_title # => 'Foo - Bar - My Project - Redmine'
510 514 def html_title(*args)
511 515 if args.empty?
512 516 title = @html_title || []
513 517 title << @project.name if @project
514 518 title << Setting.app_title unless Setting.app_title == title.last
515 519 title.reject(&:blank?).join(' - ')
516 520 else
517 521 @html_title ||= []
518 522 @html_title += args
519 523 end
520 524 end
521 525
522 526 # Returns the theme, controller name, and action as css classes for the
523 527 # HTML body.
524 528 def body_css_classes
525 529 css = []
526 530 if theme = Redmine::Themes.theme(Setting.ui_theme)
527 531 css << 'theme-' + theme.name
528 532 end
529 533
530 534 css << 'project-' + @project.identifier if @project && @project.identifier.present?
531 535 css << 'controller-' + controller_name
532 536 css << 'action-' + action_name
533 537 css.join(' ')
534 538 end
535 539
536 540 def accesskey(s)
537 541 @used_accesskeys ||= []
538 542 key = Redmine::AccessKeys.key_for(s)
539 543 return nil if @used_accesskeys.include?(key)
540 544 @used_accesskeys << key
541 545 key
542 546 end
543 547
544 548 # Formats text according to system settings.
545 549 # 2 ways to call this method:
546 550 # * with a String: textilizable(text, options)
547 551 # * with an object and one of its attribute: textilizable(issue, :description, options)
548 552 def textilizable(*args)
549 553 options = args.last.is_a?(Hash) ? args.pop : {}
550 554 case args.size
551 555 when 1
552 556 obj = options[:object]
553 557 text = args.shift
554 558 when 2
555 559 obj = args.shift
556 560 attr = args.shift
557 561 text = obj.send(attr).to_s
558 562 else
559 563 raise ArgumentError, 'invalid arguments to textilizable'
560 564 end
561 565 return '' if text.blank?
562 566 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
563 567 @only_path = only_path = options.delete(:only_path) == false ? false : true
564 568
565 569 text = text.dup
566 570 macros = catch_macros(text)
567 571 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
568 572
569 573 @parsed_headings = []
570 574 @heading_anchors = {}
571 575 @current_section = 0 if options[:edit_section_links]
572 576
573 577 parse_sections(text, project, obj, attr, only_path, options)
574 578 text = parse_non_pre_blocks(text, obj, macros) do |text|
575 579 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
576 580 send method_name, text, project, obj, attr, only_path, options
577 581 end
578 582 end
579 583 parse_headings(text, project, obj, attr, only_path, options)
580 584
581 585 if @parsed_headings.any?
582 586 replace_toc(text, @parsed_headings)
583 587 end
584 588
585 589 text.html_safe
586 590 end
587 591
588 592 def parse_non_pre_blocks(text, obj, macros)
589 593 s = StringScanner.new(text)
590 594 tags = []
591 595 parsed = ''
592 596 while !s.eos?
593 597 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
594 598 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
595 599 if tags.empty?
596 600 yield text
597 601 inject_macros(text, obj, macros) if macros.any?
598 602 else
599 603 inject_macros(text, obj, macros, false) if macros.any?
600 604 end
601 605 parsed << text
602 606 if tag
603 607 if closing
604 608 if tags.last == tag.downcase
605 609 tags.pop
606 610 end
607 611 else
608 612 tags << tag.downcase
609 613 end
610 614 parsed << full_tag
611 615 end
612 616 end
613 617 # Close any non closing tags
614 618 while tag = tags.pop
615 619 parsed << "</#{tag}>"
616 620 end
617 621 parsed
618 622 end
619 623
620 624 def parse_inline_attachments(text, project, obj, attr, only_path, options)
621 625 # when using an image link, try to use an attachment, if possible
622 626 attachments = options[:attachments] || []
623 627 attachments += obj.attachments if obj.respond_to?(:attachments)
624 628 if attachments.present?
625 629 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
626 630 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
627 631 # search for the picture in attachments
628 632 if found = Attachment.latest_attach(attachments, filename)
629 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
633 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
630 634 desc = found.description.to_s.gsub('"', '')
631 635 if !desc.blank? && alttext.blank?
632 636 alt = " title=\"#{desc}\" alt=\"#{desc}\""
633 637 end
634 638 "src=\"#{image_url}\"#{alt}"
635 639 else
636 640 m
637 641 end
638 642 end
639 643 end
640 644 end
641 645
642 646 # Wiki links
643 647 #
644 648 # Examples:
645 649 # [[mypage]]
646 650 # [[mypage|mytext]]
647 651 # wiki links can refer other project wikis, using project name or identifier:
648 652 # [[project:]] -> wiki starting page
649 653 # [[project:|mytext]]
650 654 # [[project:mypage]]
651 655 # [[project:mypage|mytext]]
652 656 def parse_wiki_links(text, project, obj, attr, only_path, options)
653 657 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
654 658 link_project = project
655 659 esc, all, page, title = $1, $2, $3, $5
656 660 if esc.nil?
657 661 if page =~ /^([^\:]+)\:(.*)$/
658 662 identifier, page = $1, $2
659 663 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
660 664 title ||= identifier if page.blank?
661 665 end
662 666
663 667 if link_project && link_project.wiki
664 668 # extract anchor
665 669 anchor = nil
666 670 if page =~ /^(.+?)\#(.+)$/
667 671 page, anchor = $1, $2
668 672 end
669 673 anchor = sanitize_anchor_name(anchor) if anchor.present?
670 674 # check if page exists
671 675 wiki_page = link_project.wiki.find_page(page)
672 676 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
673 677 "##{anchor}"
674 678 else
675 679 case options[:wiki_links]
676 680 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
677 681 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
678 682 else
679 683 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
680 684 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
681 685 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
682 686 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
683 687 end
684 688 end
685 689 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
686 690 else
687 691 # project or wiki doesn't exist
688 692 all
689 693 end
690 694 else
691 695 all
692 696 end
693 697 end
694 698 end
695 699
696 700 # Redmine links
697 701 #
698 702 # Examples:
699 703 # Issues:
700 704 # #52 -> Link to issue #52
701 705 # Changesets:
702 706 # r52 -> Link to revision 52
703 707 # commit:a85130f -> Link to scmid starting with a85130f
704 708 # Documents:
705 709 # document#17 -> Link to document with id 17
706 710 # document:Greetings -> Link to the document with title "Greetings"
707 711 # document:"Some document" -> Link to the document with title "Some document"
708 712 # Versions:
709 713 # version#3 -> Link to version with id 3
710 714 # version:1.0.0 -> Link to version named "1.0.0"
711 715 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
712 716 # Attachments:
713 717 # attachment:file.zip -> Link to the attachment of the current object named file.zip
714 718 # Source files:
715 719 # source:some/file -> Link to the file located at /some/file in the project's repository
716 720 # source:some/file@52 -> Link to the file's revision 52
717 721 # source:some/file#L120 -> Link to line 120 of the file
718 722 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
719 723 # export:some/file -> Force the download of the file
720 724 # Forum messages:
721 725 # message#1218 -> Link to message with id 1218
722 726 # Projects:
723 727 # project:someproject -> Link to project named "someproject"
724 728 # project#3 -> Link to project with id 3
725 729 #
726 730 # Links can refer other objects from other projects, using project identifier:
727 731 # identifier:r52
728 732 # identifier:document:"Some document"
729 733 # identifier:version:1.0.0
730 734 # identifier:source:some/file
731 735 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
732 736 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
733 737 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
734 738 if tag_content
735 739 $&
736 740 else
737 741 link = nil
738 742 project = default_project
739 743 if project_identifier
740 744 project = Project.visible.find_by_identifier(project_identifier)
741 745 end
742 746 if esc.nil?
743 747 if prefix.nil? && sep == 'r'
744 748 if project
745 749 repository = nil
746 750 if repo_identifier
747 751 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
748 752 else
749 753 repository = project.repository
750 754 end
751 755 # project.changesets.visible raises an SQL error because of a double join on repositories
752 756 if repository &&
753 757 (changeset = Changeset.visible.
754 758 find_by_repository_id_and_revision(repository.id, identifier))
755 759 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
756 760 {:only_path => only_path, :controller => 'repositories',
757 761 :action => 'revision', :id => project,
758 762 :repository_id => repository.identifier_param,
759 763 :rev => changeset.revision},
760 764 :class => 'changeset',
761 765 :title => truncate_single_line_raw(changeset.comments, 100))
762 766 end
763 767 end
764 768 elsif sep == '#'
765 769 oid = identifier.to_i
766 770 case prefix
767 771 when nil
768 772 if oid.to_s == identifier &&
769 773 issue = Issue.visible.find_by_id(oid)
770 774 anchor = comment_id ? "note-#{comment_id}" : nil
771 775 link = link_to("##{oid}#{comment_suffix}",
772 issue_path(issue, :only_path => only_path, :anchor => anchor),
776 issue_url(issue, :only_path => only_path, :anchor => anchor),
773 777 :class => issue.css_classes,
774 778 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
775 779 end
776 780 when 'document'
777 781 if document = Document.visible.find_by_id(oid)
778 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
782 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
779 783 end
780 784 when 'version'
781 785 if version = Version.visible.find_by_id(oid)
782 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
786 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
783 787 end
784 788 when 'message'
785 789 if message = Message.visible.find_by_id(oid)
786 790 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
787 791 end
788 792 when 'forum'
789 793 if board = Board.visible.find_by_id(oid)
790 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
794 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
791 795 end
792 796 when 'news'
793 797 if news = News.visible.find_by_id(oid)
794 link = link_to(news.title, news_path(news, :only_path => only_path), :class => 'news')
798 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
795 799 end
796 800 when 'project'
797 801 if p = Project.visible.find_by_id(oid)
798 802 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
799 803 end
800 804 end
801 805 elsif sep == ':'
802 806 # removes the double quotes if any
803 807 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
804 808 name = CGI.unescapeHTML(name)
805 809 case prefix
806 810 when 'document'
807 811 if project && document = project.documents.visible.find_by_title(name)
808 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
812 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
809 813 end
810 814 when 'version'
811 815 if project && version = project.versions.visible.find_by_name(name)
812 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
816 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
813 817 end
814 818 when 'forum'
815 819 if project && board = project.boards.visible.find_by_name(name)
816 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
820 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
817 821 end
818 822 when 'news'
819 823 if project && news = project.news.visible.find_by_title(name)
820 link = link_to(news.title, news_path(news, :only_path => only_path), :class => 'news')
824 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
821 825 end
822 826 when 'commit', 'source', 'export'
823 827 if project
824 828 repository = nil
825 829 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
826 830 repo_prefix, repo_identifier, name = $1, $2, $3
827 831 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
828 832 else
829 833 repository = project.repository
830 834 end
831 835 if prefix == 'commit'
832 836 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
833 837 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
834 838 :class => 'changeset',
835 839 :title => truncate_single_line_raw(changeset.comments, 100)
836 840 end
837 841 else
838 842 if repository && User.current.allowed_to?(:browse_repository, project)
839 843 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
840 844 path, rev, anchor = $1, $3, $5
841 845 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
842 846 :path => to_path_param(path),
843 847 :rev => rev,
844 848 :anchor => anchor},
845 849 :class => (prefix == 'export' ? 'source download' : 'source')
846 850 end
847 851 end
848 852 repo_prefix = nil
849 853 end
850 854 when 'attachment'
851 855 attachments = options[:attachments] || []
852 856 attachments += obj.attachments if obj.respond_to?(:attachments)
853 857 if attachments && attachment = Attachment.latest_attach(attachments, name)
854 858 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
855 859 end
856 860 when 'project'
857 861 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
858 862 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
859 863 end
860 864 end
861 865 end
862 866 end
863 867 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
864 868 end
865 869 end
866 870 end
867 871
868 872 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
869 873
870 874 def parse_sections(text, project, obj, attr, only_path, options)
871 875 return unless options[:edit_section_links]
872 876 text.gsub!(HEADING_RE) do
873 877 heading = $1
874 878 @current_section += 1
875 879 if @current_section > 1
876 880 content_tag('div',
877 881 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
878 882 :class => 'contextual',
879 883 :title => l(:button_edit_section),
880 884 :id => "section-#{@current_section}") + heading.html_safe
881 885 else
882 886 heading
883 887 end
884 888 end
885 889 end
886 890
887 891 # Headings and TOC
888 892 # Adds ids and links to headings unless options[:headings] is set to false
889 893 def parse_headings(text, project, obj, attr, only_path, options)
890 894 return if options[:headings] == false
891 895
892 896 text.gsub!(HEADING_RE) do
893 897 level, attrs, content = $2.to_i, $3, $4
894 898 item = strip_tags(content).strip
895 899 anchor = sanitize_anchor_name(item)
896 900 # used for single-file wiki export
897 901 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
898 902 @heading_anchors[anchor] ||= 0
899 903 idx = (@heading_anchors[anchor] += 1)
900 904 if idx > 1
901 905 anchor = "#{anchor}-#{idx}"
902 906 end
903 907 @parsed_headings << [level, anchor, item]
904 908 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
905 909 end
906 910 end
907 911
908 912 MACROS_RE = /(
909 913 (!)? # escaping
910 914 (
911 915 \{\{ # opening tag
912 916 ([\w]+) # macro name
913 917 (\(([^\n\r]*?)\))? # optional arguments
914 918 ([\n\r].*?[\n\r])? # optional block of text
915 919 \}\} # closing tag
916 920 )
917 921 )/mx unless const_defined?(:MACROS_RE)
918 922
919 923 MACRO_SUB_RE = /(
920 924 \{\{
921 925 macro\((\d+)\)
922 926 \}\}
923 927 )/x unless const_defined?(:MACRO_SUB_RE)
924 928
925 929 # Extracts macros from text
926 930 def catch_macros(text)
927 931 macros = {}
928 932 text.gsub!(MACROS_RE) do
929 933 all, macro = $1, $4.downcase
930 934 if macro_exists?(macro) || all =~ MACRO_SUB_RE
931 935 index = macros.size
932 936 macros[index] = all
933 937 "{{macro(#{index})}}"
934 938 else
935 939 all
936 940 end
937 941 end
938 942 macros
939 943 end
940 944
941 945 # Executes and replaces macros in text
942 946 def inject_macros(text, obj, macros, execute=true)
943 947 text.gsub!(MACRO_SUB_RE) do
944 948 all, index = $1, $2.to_i
945 949 orig = macros.delete(index)
946 950 if execute && orig && orig =~ MACROS_RE
947 951 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
948 952 if esc.nil?
949 953 h(exec_macro(macro, obj, args, block) || all)
950 954 else
951 955 h(all)
952 956 end
953 957 elsif orig
954 958 h(orig)
955 959 else
956 960 h(all)
957 961 end
958 962 end
959 963 end
960 964
961 965 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
962 966
963 967 # Renders the TOC with given headings
964 968 def replace_toc(text, headings)
965 969 text.gsub!(TOC_RE) do
966 970 left_align, right_align = $2, $3
967 971 # Keep only the 4 first levels
968 972 headings = headings.select{|level, anchor, item| level <= 4}
969 973 if headings.empty?
970 974 ''
971 975 else
972 976 div_class = 'toc'
973 977 div_class << ' right' if right_align
974 978 div_class << ' left' if left_align
975 979 out = "<ul class=\"#{div_class}\"><li>"
976 980 root = headings.map(&:first).min
977 981 current = root
978 982 started = false
979 983 headings.each do |level, anchor, item|
980 984 if level > current
981 985 out << '<ul><li>' * (level - current)
982 986 elsif level < current
983 987 out << "</li></ul>\n" * (current - level) + "</li><li>"
984 988 elsif started
985 989 out << '</li><li>'
986 990 end
987 991 out << "<a href=\"##{anchor}\">#{item}</a>"
988 992 current = level
989 993 started = true
990 994 end
991 995 out << '</li></ul>' * (current - root)
992 996 out << '</li></ul>'
993 997 end
994 998 end
995 999 end
996 1000
997 1001 # Same as Rails' simple_format helper without using paragraphs
998 1002 def simple_format_without_paragraph(text)
999 1003 text.to_s.
1000 1004 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1001 1005 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1002 1006 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1003 1007 html_safe
1004 1008 end
1005 1009
1006 1010 def lang_options_for_select(blank=true)
1007 1011 (blank ? [["(auto)", ""]] : []) + languages_options
1008 1012 end
1009 1013
1010 1014 def labelled_form_for(*args, &proc)
1011 1015 args << {} unless args.last.is_a?(Hash)
1012 1016 options = args.last
1013 1017 if args.first.is_a?(Symbol)
1014 1018 options.merge!(:as => args.shift)
1015 1019 end
1016 1020 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1017 1021 form_for(*args, &proc)
1018 1022 end
1019 1023
1020 1024 def labelled_fields_for(*args, &proc)
1021 1025 args << {} unless args.last.is_a?(Hash)
1022 1026 options = args.last
1023 1027 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1024 1028 fields_for(*args, &proc)
1025 1029 end
1026 1030
1027 1031 def error_messages_for(*objects)
1028 1032 html = ""
1029 1033 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1030 1034 errors = objects.map {|o| o.errors.full_messages}.flatten
1031 1035 if errors.any?
1032 1036 html << "<div id='errorExplanation'><ul>\n"
1033 1037 errors.each do |error|
1034 1038 html << "<li>#{h error}</li>\n"
1035 1039 end
1036 1040 html << "</ul></div>\n"
1037 1041 end
1038 1042 html.html_safe
1039 1043 end
1040 1044
1041 1045 def delete_link(url, options={})
1042 1046 options = {
1043 1047 :method => :delete,
1044 1048 :data => {:confirm => l(:text_are_you_sure)},
1045 1049 :class => 'icon icon-del'
1046 1050 }.merge(options)
1047 1051
1048 1052 link_to l(:button_delete), url, options
1049 1053 end
1050 1054
1051 1055 def preview_link(url, form, target='preview', options={})
1052 1056 content_tag 'a', l(:label_preview), {
1053 1057 :href => "#",
1054 1058 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1055 1059 :accesskey => accesskey(:preview)
1056 1060 }.merge(options)
1057 1061 end
1058 1062
1059 1063 def link_to_function(name, function, html_options={})
1060 1064 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1061 1065 end
1062 1066
1063 1067 # Helper to render JSON in views
1064 1068 def raw_json(arg)
1065 1069 arg.to_json.to_s.gsub('/', '\/').html_safe
1066 1070 end
1067 1071
1068 1072 def back_url
1069 1073 url = params[:back_url]
1070 1074 if url.nil? && referer = request.env['HTTP_REFERER']
1071 1075 url = CGI.unescape(referer.to_s)
1072 1076 end
1073 1077 url
1074 1078 end
1075 1079
1076 1080 def back_url_hidden_field_tag
1077 1081 url = back_url
1078 1082 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1079 1083 end
1080 1084
1081 1085 def check_all_links(form_name)
1082 1086 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1083 1087 " | ".html_safe +
1084 1088 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1085 1089 end
1086 1090
1087 1091 def toggle_checkboxes_link(selector)
1088 1092 link_to_function image_tag('toggle_check.png'),
1089 1093 "toggleCheckboxesBySelector('#{selector}')",
1090 1094 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1091 1095 end
1092 1096
1093 1097 def progress_bar(pcts, options={})
1094 1098 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1095 1099 pcts = pcts.collect(&:round)
1096 1100 pcts[1] = pcts[1] - pcts[0]
1097 1101 pcts << (100 - pcts[1] - pcts[0])
1098 1102 width = options[:width] || '100px;'
1099 1103 legend = options[:legend] || ''
1100 1104 content_tag('table',
1101 1105 content_tag('tr',
1102 1106 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1103 1107 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1104 1108 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1105 1109 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1106 1110 content_tag('p', legend, :class => 'percent').html_safe
1107 1111 end
1108 1112
1109 1113 def checked_image(checked=true)
1110 1114 if checked
1111 1115 image_tag 'toggle_check.png'
1112 1116 end
1113 1117 end
1114 1118
1115 1119 def context_menu(url)
1116 1120 unless @context_menu_included
1117 1121 content_for :header_tags do
1118 1122 javascript_include_tag('context_menu') +
1119 1123 stylesheet_link_tag('context_menu')
1120 1124 end
1121 1125 if l(:direction) == 'rtl'
1122 1126 content_for :header_tags do
1123 1127 stylesheet_link_tag('context_menu_rtl')
1124 1128 end
1125 1129 end
1126 1130 @context_menu_included = true
1127 1131 end
1128 1132 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1129 1133 end
1130 1134
1131 1135 def calendar_for(field_id)
1132 1136 include_calendar_headers_tags
1133 1137 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1134 1138 end
1135 1139
1136 1140 def include_calendar_headers_tags
1137 1141 unless @calendar_headers_tags_included
1138 1142 tags = ''.html_safe
1139 1143 @calendar_headers_tags_included = true
1140 1144 content_for :header_tags do
1141 1145 start_of_week = Setting.start_of_week
1142 1146 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1143 1147 # Redmine uses 1..7 (monday..sunday) in settings and locales
1144 1148 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1145 1149 start_of_week = start_of_week.to_i % 7
1146 1150 tags << javascript_tag(
1147 1151 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1148 1152 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1149 1153 path_to_image('/images/calendar.png') +
1150 1154 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1151 1155 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1152 1156 "beforeShow: beforeShowDatePicker};")
1153 1157 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1154 1158 unless jquery_locale == 'en'
1155 1159 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1156 1160 end
1157 1161 tags
1158 1162 end
1159 1163 end
1160 1164 end
1161 1165
1162 1166 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1163 1167 # Examples:
1164 1168 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1165 1169 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1166 1170 #
1167 1171 def stylesheet_link_tag(*sources)
1168 1172 options = sources.last.is_a?(Hash) ? sources.pop : {}
1169 1173 plugin = options.delete(:plugin)
1170 1174 sources = sources.map do |source|
1171 1175 if plugin
1172 1176 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1173 1177 elsif current_theme && current_theme.stylesheets.include?(source)
1174 1178 current_theme.stylesheet_path(source)
1175 1179 else
1176 1180 source
1177 1181 end
1178 1182 end
1179 1183 super *sources, options
1180 1184 end
1181 1185
1182 1186 # Overrides Rails' image_tag with themes and plugins support.
1183 1187 # Examples:
1184 1188 # image_tag('image.png') # => picks image.png from the current theme or defaults
1185 1189 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1186 1190 #
1187 1191 def image_tag(source, options={})
1188 1192 if plugin = options.delete(:plugin)
1189 1193 source = "/plugin_assets/#{plugin}/images/#{source}"
1190 1194 elsif current_theme && current_theme.images.include?(source)
1191 1195 source = current_theme.image_path(source)
1192 1196 end
1193 1197 super source, options
1194 1198 end
1195 1199
1196 1200 # Overrides Rails' javascript_include_tag with plugins support
1197 1201 # Examples:
1198 1202 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1199 1203 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1200 1204 #
1201 1205 def javascript_include_tag(*sources)
1202 1206 options = sources.last.is_a?(Hash) ? sources.pop : {}
1203 1207 if plugin = options.delete(:plugin)
1204 1208 sources = sources.map do |source|
1205 1209 if plugin
1206 1210 "/plugin_assets/#{plugin}/javascripts/#{source}"
1207 1211 else
1208 1212 source
1209 1213 end
1210 1214 end
1211 1215 end
1212 1216 super *sources, options
1213 1217 end
1214 1218
1215 1219 def sidebar_content?
1216 1220 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1217 1221 end
1218 1222
1219 1223 def view_layouts_base_sidebar_hook_response
1220 1224 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1221 1225 end
1222 1226
1223 1227 def email_delivery_enabled?
1224 1228 !!ActionMailer::Base.perform_deliveries
1225 1229 end
1226 1230
1227 1231 # Returns the avatar image tag for the given +user+ if avatars are enabled
1228 1232 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1229 1233 def avatar(user, options = { })
1230 1234 if Setting.gravatar_enabled?
1231 1235 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1232 1236 email = nil
1233 1237 if user.respond_to?(:mail)
1234 1238 email = user.mail
1235 1239 elsif user.to_s =~ %r{<(.+?)>}
1236 1240 email = $1
1237 1241 end
1238 1242 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1239 1243 else
1240 1244 ''
1241 1245 end
1242 1246 end
1243 1247
1244 1248 def sanitize_anchor_name(anchor)
1245 1249 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1246 1250 end
1247 1251
1248 1252 # Returns the javascript tags that are included in the html layout head
1249 1253 def javascript_heads
1250 1254 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1251 1255 unless User.current.pref.warn_on_leaving_unsaved == '0'
1252 1256 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1253 1257 end
1254 1258 tags
1255 1259 end
1256 1260
1257 1261 def favicon
1258 1262 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1259 1263 end
1260 1264
1261 1265 # Returns the path to the favicon
1262 1266 def favicon_path
1263 1267 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1264 1268 image_path(icon)
1265 1269 end
1266 1270
1267 1271 # Returns the full URL to the favicon
1268 1272 def favicon_url
1269 1273 # TODO: use #image_url introduced in Rails4
1270 1274 path = favicon_path
1271 1275 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1272 1276 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1273 1277 end
1274 1278
1275 1279 def robot_exclusion_tag
1276 1280 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1277 1281 end
1278 1282
1279 1283 # Returns true if arg is expected in the API response
1280 1284 def include_in_api_response?(arg)
1281 1285 unless @included_in_api_response
1282 1286 param = params[:include]
1283 1287 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1284 1288 @included_in_api_response.collect!(&:strip)
1285 1289 end
1286 1290 @included_in_api_response.include?(arg.to_s)
1287 1291 end
1288 1292
1289 1293 # Returns options or nil if nometa param or X-Redmine-Nometa header
1290 1294 # was set in the request
1291 1295 def api_meta(options)
1292 1296 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1293 1297 # compatibility mode for activeresource clients that raise
1294 1298 # an error when deserializing an array with attributes
1295 1299 nil
1296 1300 else
1297 1301 options
1298 1302 end
1299 1303 end
1300 1304
1301 1305 private
1302 1306
1303 1307 def wiki_helper
1304 1308 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1305 1309 extend helper
1306 1310 return self
1307 1311 end
1308 1312
1309 1313 def link_to_content_update(text, url_params = {}, html_options = {})
1310 1314 link_to(text, url_params, html_options)
1311 1315 end
1312 1316 end
@@ -1,1561 +1,1560
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 62 attr_reader :current_journal
63 63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 64
65 65 validates_presence_of :subject, :project, :tracker
66 66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 69
70 70 validates_length_of :subject, :maximum => 255
71 71 validates_inclusion_of :done_ratio, :in => 0..100
72 72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 73 validates :start_date, :date => true
74 74 validates :due_date, :date => true
75 75 validate :validate_issue, :validate_required_fields
76 76 attr_protected :id
77 77
78 78 scope :visible, lambda {|*args|
79 includes(:project).
80 references(:project).
79 joins(:project).
81 80 where(Issue.visible_condition(args.shift || User.current, *args))
82 81 }
83 82
84 83 scope :open, lambda {|*args|
85 84 is_closed = args.size > 0 ? !args.first : false
86 85 joins(:status).
87 86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
88 87 }
89 88
90 89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
91 90 scope :on_active_project, lambda {
92 91 joins(:project).
93 92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
94 93 }
95 94 scope :fixed_version, lambda {|versions|
96 95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
97 96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
98 97 }
99 98
100 99 before_create :default_assign
101 100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
102 101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
103 102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
104 103 after_save :reschedule_following_issues, :update_nested_set_attributes,
105 104 :update_parent_attributes, :create_journal
106 105 # Should be after_create but would be called before previous after_save callbacks
107 106 after_save :after_create_from_copy
108 107 after_destroy :update_parent_attributes
109 108 after_create :send_notification
110 109 # Keep it at the end of after_save callbacks
111 110 after_save :clear_assigned_to_was
112 111
113 112 # Returns a SQL conditions string used to find all issues visible by the specified user
114 113 def self.visible_condition(user, options={})
115 114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
116 115 if user.id && user.logged?
117 116 case role.issues_visibility
118 117 when 'all'
119 118 nil
120 119 when 'default'
121 120 user_ids = [user.id] + user.groups.map(&:id).compact
122 121 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
123 122 when 'own'
124 123 user_ids = [user.id] + user.groups.map(&:id).compact
125 124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
126 125 else
127 126 '1=0'
128 127 end
129 128 else
130 129 "(#{table_name}.is_private = #{connection.quoted_false})"
131 130 end
132 131 end
133 132 end
134 133
135 134 # Returns true if usr or current user is allowed to view the issue
136 135 def visible?(usr=nil)
137 136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
138 137 if user.logged?
139 138 case role.issues_visibility
140 139 when 'all'
141 140 true
142 141 when 'default'
143 142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
144 143 when 'own'
145 144 self.author == user || user.is_or_belongs_to?(assigned_to)
146 145 else
147 146 false
148 147 end
149 148 else
150 149 !self.is_private?
151 150 end
152 151 end
153 152 end
154 153
155 154 # Returns true if user or current user is allowed to edit or add a note to the issue
156 155 def editable?(user=User.current)
157 156 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
158 157 end
159 158
160 159 def initialize(attributes=nil, *args)
161 160 super
162 161 if new_record?
163 162 # set default values for new records only
164 163 self.priority ||= IssuePriority.default
165 164 self.watcher_user_ids = []
166 165 end
167 166 end
168 167
169 168 def create_or_update
170 169 super
171 170 ensure
172 171 @status_was = nil
173 172 end
174 173 private :create_or_update
175 174
176 175 # AR#Persistence#destroy would raise and RecordNotFound exception
177 176 # if the issue was already deleted or updated (non matching lock_version).
178 177 # This is a problem when bulk deleting issues or deleting a project
179 178 # (because an issue may already be deleted if its parent was deleted
180 179 # first).
181 180 # The issue is reloaded by the nested_set before being deleted so
182 181 # the lock_version condition should not be an issue but we handle it.
183 182 def destroy
184 183 super
185 184 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
186 185 # Stale or already deleted
187 186 begin
188 187 reload
189 188 rescue ActiveRecord::RecordNotFound
190 189 # The issue was actually already deleted
191 190 @destroyed = true
192 191 return freeze
193 192 end
194 193 # The issue was stale, retry to destroy
195 194 super
196 195 end
197 196
198 197 alias :base_reload :reload
199 198 def reload(*args)
200 199 @workflow_rule_by_attribute = nil
201 200 @assignable_versions = nil
202 201 @relations = nil
203 202 @spent_hours = nil
204 203 base_reload(*args)
205 204 end
206 205
207 206 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
208 207 def available_custom_fields
209 208 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
210 209 end
211 210
212 211 def visible_custom_field_values(user=nil)
213 212 user_real = user || User.current
214 213 custom_field_values.select do |value|
215 214 value.custom_field.visible_by?(project, user_real)
216 215 end
217 216 end
218 217
219 218 # Copies attributes from another issue, arg can be an id or an Issue
220 219 def copy_from(arg, options={})
221 220 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
222 221 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
223 222 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
224 223 self.status = issue.status
225 224 self.author = User.current
226 225 unless options[:attachments] == false
227 226 self.attachments = issue.attachments.map do |attachement|
228 227 attachement.copy(:container => self)
229 228 end
230 229 end
231 230 @copied_from = issue
232 231 @copy_options = options
233 232 self
234 233 end
235 234
236 235 # Returns an unsaved copy of the issue
237 236 def copy(attributes=nil, copy_options={})
238 237 copy = self.class.new.copy_from(self, copy_options)
239 238 copy.attributes = attributes if attributes
240 239 copy
241 240 end
242 241
243 242 # Returns true if the issue is a copy
244 243 def copy?
245 244 @copied_from.present?
246 245 end
247 246
248 247 def status_id=(status_id)
249 248 if status_id.to_s != self.status_id.to_s
250 249 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
251 250 end
252 251 self.status_id
253 252 end
254 253
255 254 # Sets the status.
256 255 def status=(status)
257 256 if status != self.status
258 257 @workflow_rule_by_attribute = nil
259 258 end
260 259 association(:status).writer(status)
261 260 end
262 261
263 262 def priority_id=(pid)
264 263 self.priority = nil
265 264 write_attribute(:priority_id, pid)
266 265 end
267 266
268 267 def category_id=(cid)
269 268 self.category = nil
270 269 write_attribute(:category_id, cid)
271 270 end
272 271
273 272 def fixed_version_id=(vid)
274 273 self.fixed_version = nil
275 274 write_attribute(:fixed_version_id, vid)
276 275 end
277 276
278 277 def tracker_id=(tracker_id)
279 278 if tracker_id.to_s != self.tracker_id.to_s
280 279 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
281 280 end
282 281 self.tracker_id
283 282 end
284 283
285 284 # Sets the tracker.
286 285 # This will set the status to the default status of the new tracker if:
287 286 # * the status was the default for the previous tracker
288 287 # * or if the status was not part of the new tracker statuses
289 288 # * or the status was nil
290 289 def tracker=(tracker)
291 290 if tracker != self.tracker
292 291 if status == default_status
293 292 self.status = nil
294 293 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
295 294 self.status = nil
296 295 end
297 296 @custom_field_values = nil
298 297 @workflow_rule_by_attribute = nil
299 298 end
300 299 association(:tracker).writer(tracker)
301 300 self.status ||= default_status
302 301 self.tracker
303 302 end
304 303
305 304 def project_id=(project_id)
306 305 if project_id.to_s != self.project_id.to_s
307 306 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
308 307 end
309 308 self.project_id
310 309 end
311 310
312 311 # Sets the project.
313 312 # Unless keep_tracker argument is set to true, this will change the tracker
314 313 # to the first tracker of the new project if the previous tracker is not part
315 314 # of the new project trackers.
316 315 # This will clear the fixed_version is it's no longer valid for the new project.
317 316 # This will clear the parent issue if it's no longer valid for the new project.
318 317 # This will set the category to the category with the same name in the new
319 318 # project if it exists, or clear it if it doesn't.
320 319 def project=(project, keep_tracker=false)
321 320 project_was = self.project
322 321 association(:project).writer(project)
323 322 if project_was && project && project_was != project
324 323 @assignable_versions = nil
325 324
326 325 unless keep_tracker || project.trackers.include?(tracker)
327 326 self.tracker = project.trackers.first
328 327 end
329 328 # Reassign to the category with same name if any
330 329 if category
331 330 self.category = project.issue_categories.find_by_name(category.name)
332 331 end
333 332 # Keep the fixed_version if it's still valid in the new_project
334 333 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
335 334 self.fixed_version = nil
336 335 end
337 336 # Clear the parent task if it's no longer valid
338 337 unless valid_parent_project?
339 338 self.parent_issue_id = nil
340 339 end
341 340 @custom_field_values = nil
342 341 @workflow_rule_by_attribute = nil
343 342 end
344 343 self.project
345 344 end
346 345
347 346 def description=(arg)
348 347 if arg.is_a?(String)
349 348 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
350 349 end
351 350 write_attribute(:description, arg)
352 351 end
353 352
354 353 # Overrides assign_attributes so that project and tracker get assigned first
355 354 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
356 355 return if new_attributes.nil?
357 356 attrs = new_attributes.dup
358 357 attrs.stringify_keys!
359 358
360 359 %w(project project_id tracker tracker_id).each do |attr|
361 360 if attrs.has_key?(attr)
362 361 send "#{attr}=", attrs.delete(attr)
363 362 end
364 363 end
365 364 send :assign_attributes_without_project_and_tracker_first, attrs, *args
366 365 end
367 366 # Do not redefine alias chain on reload (see #4838)
368 367 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
369 368
370 369 def attributes=(new_attributes)
371 370 assign_attributes new_attributes
372 371 end
373 372
374 373 def estimated_hours=(h)
375 374 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
376 375 end
377 376
378 377 safe_attributes 'project_id',
379 378 :if => lambda {|issue, user|
380 379 if issue.new_record?
381 380 issue.copy?
382 381 elsif user.allowed_to?(:move_issues, issue.project)
383 382 Issue.allowed_target_projects_on_move.count > 1
384 383 end
385 384 }
386 385
387 386 safe_attributes 'tracker_id',
388 387 'status_id',
389 388 'category_id',
390 389 'assigned_to_id',
391 390 'priority_id',
392 391 'fixed_version_id',
393 392 'subject',
394 393 'description',
395 394 'start_date',
396 395 'due_date',
397 396 'done_ratio',
398 397 'estimated_hours',
399 398 'custom_field_values',
400 399 'custom_fields',
401 400 'lock_version',
402 401 'notes',
403 402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
404 403
405 404 safe_attributes 'notes',
406 405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
407 406
408 407 safe_attributes 'private_notes',
409 408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
410 409
411 410 safe_attributes 'watcher_user_ids',
412 411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
413 412
414 413 safe_attributes 'is_private',
415 414 :if => lambda {|issue, user|
416 415 user.allowed_to?(:set_issues_private, issue.project) ||
417 416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
418 417 }
419 418
420 419 safe_attributes 'parent_issue_id',
421 420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
422 421 user.allowed_to?(:manage_subtasks, issue.project)}
423 422
424 423 def safe_attribute_names(user=nil)
425 424 names = super
426 425 names -= disabled_core_fields
427 426 names -= read_only_attribute_names(user)
428 427 names
429 428 end
430 429
431 430 # Safely sets attributes
432 431 # Should be called from controllers instead of #attributes=
433 432 # attr_accessible is too rough because we still want things like
434 433 # Issue.new(:project => foo) to work
435 434 def safe_attributes=(attrs, user=User.current)
436 435 return unless attrs.is_a?(Hash)
437 436
438 437 attrs = attrs.deep_dup
439 438
440 439 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
441 440 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
442 441 if allowed_target_projects(user).where(:id => p.to_i).exists?
443 442 self.project_id = p
444 443 end
445 444 end
446 445
447 446 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
448 447 self.tracker_id = t
449 448 end
450 449
451 450 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
452 451 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
453 452 self.status_id = s
454 453 end
455 454 end
456 455
457 456 attrs = delete_unsafe_attributes(attrs, user)
458 457 return if attrs.empty?
459 458
460 459 unless leaf?
461 460 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
462 461 end
463 462
464 463 if attrs['parent_issue_id'].present?
465 464 s = attrs['parent_issue_id'].to_s
466 465 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
467 466 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
468 467 end
469 468 end
470 469
471 470 if attrs['custom_field_values'].present?
472 471 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
473 472 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
474 473 end
475 474
476 475 if attrs['custom_fields'].present?
477 476 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
478 477 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
479 478 end
480 479
481 480 # mass-assignment security bypass
482 481 assign_attributes attrs, :without_protection => true
483 482 end
484 483
485 484 def disabled_core_fields
486 485 tracker ? tracker.disabled_core_fields : []
487 486 end
488 487
489 488 # Returns the custom_field_values that can be edited by the given user
490 489 def editable_custom_field_values(user=nil)
491 490 visible_custom_field_values(user).reject do |value|
492 491 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
493 492 end
494 493 end
495 494
496 495 # Returns the custom fields that can be edited by the given user
497 496 def editable_custom_fields(user=nil)
498 497 editable_custom_field_values(user).map(&:custom_field).uniq
499 498 end
500 499
501 500 # Returns the names of attributes that are read-only for user or the current user
502 501 # For users with multiple roles, the read-only fields are the intersection of
503 502 # read-only fields of each role
504 503 # The result is an array of strings where sustom fields are represented with their ids
505 504 #
506 505 # Examples:
507 506 # issue.read_only_attribute_names # => ['due_date', '2']
508 507 # issue.read_only_attribute_names(user) # => []
509 508 def read_only_attribute_names(user=nil)
510 509 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
511 510 end
512 511
513 512 # Returns the names of required attributes for user or the current user
514 513 # For users with multiple roles, the required fields are the intersection of
515 514 # required fields of each role
516 515 # The result is an array of strings where sustom fields are represented with their ids
517 516 #
518 517 # Examples:
519 518 # issue.required_attribute_names # => ['due_date', '2']
520 519 # issue.required_attribute_names(user) # => []
521 520 def required_attribute_names(user=nil)
522 521 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
523 522 end
524 523
525 524 # Returns true if the attribute is required for user
526 525 def required_attribute?(name, user=nil)
527 526 required_attribute_names(user).include?(name.to_s)
528 527 end
529 528
530 529 # Returns a hash of the workflow rule by attribute for the given user
531 530 #
532 531 # Examples:
533 532 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
534 533 def workflow_rule_by_attribute(user=nil)
535 534 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
536 535
537 536 user_real = user || User.current
538 537 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
539 538 roles = roles.select(&:consider_workflow?)
540 539 return {} if roles.empty?
541 540
542 541 result = {}
543 542 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
544 543 if workflow_permissions.any?
545 544 workflow_rules = workflow_permissions.inject({}) do |h, wp|
546 545 h[wp.field_name] ||= []
547 546 h[wp.field_name] << wp.rule
548 547 h
549 548 end
550 549 workflow_rules.each do |attr, rules|
551 550 next if rules.size < roles.size
552 551 uniq_rules = rules.uniq
553 552 if uniq_rules.size == 1
554 553 result[attr] = uniq_rules.first
555 554 else
556 555 result[attr] = 'required'
557 556 end
558 557 end
559 558 end
560 559 @workflow_rule_by_attribute = result if user.nil?
561 560 result
562 561 end
563 562 private :workflow_rule_by_attribute
564 563
565 564 def done_ratio
566 565 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
567 566 status.default_done_ratio
568 567 else
569 568 read_attribute(:done_ratio)
570 569 end
571 570 end
572 571
573 572 def self.use_status_for_done_ratio?
574 573 Setting.issue_done_ratio == 'issue_status'
575 574 end
576 575
577 576 def self.use_field_for_done_ratio?
578 577 Setting.issue_done_ratio == 'issue_field'
579 578 end
580 579
581 580 def validate_issue
582 581 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
583 582 errors.add :due_date, :greater_than_start_date
584 583 end
585 584
586 585 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
587 586 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
588 587 end
589 588
590 589 if fixed_version
591 590 if !assignable_versions.include?(fixed_version)
592 591 errors.add :fixed_version_id, :inclusion
593 592 elsif reopening? && fixed_version.closed?
594 593 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
595 594 end
596 595 end
597 596
598 597 # Checks that the issue can not be added/moved to a disabled tracker
599 598 if project && (tracker_id_changed? || project_id_changed?)
600 599 unless project.trackers.include?(tracker)
601 600 errors.add :tracker_id, :inclusion
602 601 end
603 602 end
604 603
605 604 # Checks parent issue assignment
606 605 if @invalid_parent_issue_id.present?
607 606 errors.add :parent_issue_id, :invalid
608 607 elsif @parent_issue
609 608 if !valid_parent_project?(@parent_issue)
610 609 errors.add :parent_issue_id, :invalid
611 610 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
612 611 errors.add :parent_issue_id, :invalid
613 612 elsif !new_record?
614 613 # moving an existing issue
615 614 if move_possible?(@parent_issue)
616 615 # move accepted
617 616 else
618 617 errors.add :parent_issue_id, :invalid
619 618 end
620 619 end
621 620 end
622 621 end
623 622
624 623 # Validates the issue against additional workflow requirements
625 624 def validate_required_fields
626 625 user = new_record? ? author : current_journal.try(:user)
627 626
628 627 required_attribute_names(user).each do |attribute|
629 628 if attribute =~ /^\d+$/
630 629 attribute = attribute.to_i
631 630 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
632 631 if v && v.value.blank?
633 632 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
634 633 end
635 634 else
636 635 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
637 636 errors.add attribute, :blank
638 637 end
639 638 end
640 639 end
641 640 end
642 641
643 642 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
644 643 # even if the user turns off the setting later
645 644 def update_done_ratio_from_issue_status
646 645 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
647 646 self.done_ratio = status.default_done_ratio
648 647 end
649 648 end
650 649
651 650 def init_journal(user, notes = "")
652 651 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
653 652 end
654 653
655 654 # Returns the current journal or nil if it's not initialized
656 655 def current_journal
657 656 @current_journal
658 657 end
659 658
660 659 # Returns the names of attributes that are journalized when updating the issue
661 660 def journalized_attribute_names
662 661 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
663 662 end
664 663
665 664 # Returns the id of the last journal or nil
666 665 def last_journal_id
667 666 if new_record?
668 667 nil
669 668 else
670 669 journals.maximum(:id)
671 670 end
672 671 end
673 672
674 673 # Returns a scope for journals that have an id greater than journal_id
675 674 def journals_after(journal_id)
676 675 scope = journals.reorder("#{Journal.table_name}.id ASC")
677 676 if journal_id.present?
678 677 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
679 678 end
680 679 scope
681 680 end
682 681
683 682 # Returns the initial status of the issue
684 683 # Returns nil for a new issue
685 684 def status_was
686 685 if status_id_changed?
687 686 if status_id_was.to_i > 0
688 687 @status_was ||= IssueStatus.find_by_id(status_id_was)
689 688 end
690 689 else
691 690 @status_was ||= status
692 691 end
693 692 end
694 693
695 694 # Return true if the issue is closed, otherwise false
696 695 def closed?
697 696 status.present? && status.is_closed?
698 697 end
699 698
700 699 # Returns true if the issue was closed when loaded
701 700 def was_closed?
702 701 status_was.present? && status_was.is_closed?
703 702 end
704 703
705 704 # Return true if the issue is being reopened
706 705 def reopening?
707 706 if new_record?
708 707 false
709 708 else
710 709 status_id_changed? && !closed? && was_closed?
711 710 end
712 711 end
713 712 alias :reopened? :reopening?
714 713
715 714 # Return true if the issue is being closed
716 715 def closing?
717 716 if new_record?
718 717 closed?
719 718 else
720 719 status_id_changed? && closed? && !was_closed?
721 720 end
722 721 end
723 722
724 723 # Returns true if the issue is overdue
725 724 def overdue?
726 725 due_date.present? && (due_date < Date.today) && !closed?
727 726 end
728 727
729 728 # Is the amount of work done less than it should for the due date
730 729 def behind_schedule?
731 730 return false if start_date.nil? || due_date.nil?
732 731 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
733 732 return done_date <= Date.today
734 733 end
735 734
736 735 # Does this issue have children?
737 736 def children?
738 737 !leaf?
739 738 end
740 739
741 740 # Users the issue can be assigned to
742 741 def assignable_users
743 742 users = project.assignable_users.to_a
744 743 users << author if author
745 744 users << assigned_to if assigned_to
746 745 users.uniq.sort
747 746 end
748 747
749 748 # Versions that the issue can be assigned to
750 749 def assignable_versions
751 750 return @assignable_versions if @assignable_versions
752 751
753 752 versions = project.shared_versions.open.to_a
754 753 if fixed_version
755 754 if fixed_version_id_changed?
756 755 # nothing to do
757 756 elsif project_id_changed?
758 757 if project.shared_versions.include?(fixed_version)
759 758 versions << fixed_version
760 759 end
761 760 else
762 761 versions << fixed_version
763 762 end
764 763 end
765 764 @assignable_versions = versions.uniq.sort
766 765 end
767 766
768 767 # Returns true if this issue is blocked by another issue that is still open
769 768 def blocked?
770 769 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
771 770 end
772 771
773 772 # Returns the default status of the issue based on its tracker
774 773 # Returns nil if tracker is nil
775 774 def default_status
776 775 tracker.try(:default_status)
777 776 end
778 777
779 778 # Returns an array of statuses that user is able to apply
780 779 def new_statuses_allowed_to(user=User.current, include_default=false)
781 780 if new_record? && @copied_from
782 781 [default_status, @copied_from.status].compact.uniq.sort
783 782 else
784 783 initial_status = nil
785 784 if new_record?
786 785 initial_status = default_status
787 786 elsif tracker_id_changed?
788 787 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
789 788 initial_status = default_status
790 789 elsif tracker.issue_status_ids.include?(status_id_was)
791 790 initial_status = IssueStatus.find_by_id(status_id_was)
792 791 else
793 792 initial_status = default_status
794 793 end
795 794 else
796 795 initial_status = status_was
797 796 end
798 797
799 798 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
800 799 assignee_transitions_allowed = initial_assigned_to_id.present? &&
801 800 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
802 801
803 802 statuses = []
804 803 if initial_status
805 804 statuses += initial_status.find_new_statuses_allowed_to(
806 805 user.admin ? Role.all.to_a : user.roles_for_project(project),
807 806 tracker,
808 807 author == user,
809 808 assignee_transitions_allowed
810 809 )
811 810 end
812 811 statuses << initial_status unless statuses.empty?
813 812 statuses << default_status if include_default
814 813 statuses = statuses.compact.uniq.sort
815 814 if blocked?
816 815 statuses.reject!(&:is_closed?)
817 816 end
818 817 statuses
819 818 end
820 819 end
821 820
822 821 # Returns the previous assignee if changed
823 822 def assigned_to_was
824 823 # assigned_to_id_was is reset before after_save callbacks
825 824 user_id = @previous_assigned_to_id || assigned_to_id_was
826 825 if user_id && user_id != assigned_to_id
827 826 @assigned_to_was ||= User.find_by_id(user_id)
828 827 end
829 828 end
830 829
831 830 # Returns the users that should be notified
832 831 def notified_users
833 832 notified = []
834 833 # Author and assignee are always notified unless they have been
835 834 # locked or don't want to be notified
836 835 notified << author if author
837 836 if assigned_to
838 837 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
839 838 end
840 839 if assigned_to_was
841 840 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
842 841 end
843 842 notified = notified.select {|u| u.active? && u.notify_about?(self)}
844 843
845 844 notified += project.notified_users
846 845 notified.uniq!
847 846 # Remove users that can not view the issue
848 847 notified.reject! {|user| !visible?(user)}
849 848 notified
850 849 end
851 850
852 851 # Returns the email addresses that should be notified
853 852 def recipients
854 853 notified_users.collect(&:mail)
855 854 end
856 855
857 856 def each_notification(users, &block)
858 857 if users.any?
859 858 if custom_field_values.detect {|value| !value.custom_field.visible?}
860 859 users_by_custom_field_visibility = users.group_by do |user|
861 860 visible_custom_field_values(user).map(&:custom_field_id).sort
862 861 end
863 862 users_by_custom_field_visibility.values.each do |users|
864 863 yield(users)
865 864 end
866 865 else
867 866 yield(users)
868 867 end
869 868 end
870 869 end
871 870
872 871 # Returns the number of hours spent on this issue
873 872 def spent_hours
874 873 @spent_hours ||= time_entries.sum(:hours) || 0
875 874 end
876 875
877 876 # Returns the total number of hours spent on this issue and its descendants
878 877 #
879 878 # Example:
880 879 # spent_hours => 0.0
881 880 # spent_hours => 50.2
882 881 def total_spent_hours
883 882 @total_spent_hours ||=
884 883 self_and_descendants.
885 884 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
886 885 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
887 886 end
888 887
889 888 def relations
890 889 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
891 890 end
892 891
893 892 # Preloads relations for a collection of issues
894 893 def self.load_relations(issues)
895 894 if issues.any?
896 895 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
897 896 issues.each do |issue|
898 897 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
899 898 end
900 899 end
901 900 end
902 901
903 902 # Preloads visible spent time for a collection of issues
904 903 def self.load_visible_spent_hours(issues, user=User.current)
905 904 if issues.any?
906 905 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
907 906 issues.each do |issue|
908 907 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
909 908 end
910 909 end
911 910 end
912 911
913 912 # Preloads visible relations for a collection of issues
914 913 def self.load_visible_relations(issues, user=User.current)
915 914 if issues.any?
916 915 issue_ids = issues.map(&:id)
917 916 # Relations with issue_from in given issues and visible issue_to
918 917 relations_from = IssueRelation.joins(:issue_to => :project).
919 918 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
920 919 # Relations with issue_to in given issues and visible issue_from
921 920 relations_to = IssueRelation.joins(:issue_from => :project).
922 921 where(visible_condition(user)).
923 922 where(:issue_to_id => issue_ids).to_a
924 923 issues.each do |issue|
925 924 relations =
926 925 relations_from.select {|relation| relation.issue_from_id == issue.id} +
927 926 relations_to.select {|relation| relation.issue_to_id == issue.id}
928 927
929 928 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
930 929 end
931 930 end
932 931 end
933 932
934 933 # Finds an issue relation given its id.
935 934 def find_relation(relation_id)
936 935 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
937 936 end
938 937
939 938 # Returns all the other issues that depend on the issue
940 939 # The algorithm is a modified breadth first search (bfs)
941 940 def all_dependent_issues(except=[])
942 941 # The found dependencies
943 942 dependencies = []
944 943
945 944 # The visited flag for every node (issue) used by the breadth first search
946 945 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
947 946
948 947 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
949 948 # the issue when it is processed.
950 949
951 950 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
952 951 # but its children will not be added to the queue when it is processed.
953 952
954 953 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
955 954 # the queue, but its children have not been added.
956 955
957 956 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
958 957 # the children still need to be processed.
959 958
960 959 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
961 960 # added as dependent issues. It needs no further processing.
962 961
963 962 issue_status = Hash.new(eNOT_DISCOVERED)
964 963
965 964 # The queue
966 965 queue = []
967 966
968 967 # Initialize the bfs, add start node (self) to the queue
969 968 queue << self
970 969 issue_status[self] = ePROCESS_ALL
971 970
972 971 while (!queue.empty?) do
973 972 current_issue = queue.shift
974 973 current_issue_status = issue_status[current_issue]
975 974 dependencies << current_issue
976 975
977 976 # Add parent to queue, if not already in it.
978 977 parent = current_issue.parent
979 978 parent_status = issue_status[parent]
980 979
981 980 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
982 981 queue << parent
983 982 issue_status[parent] = ePROCESS_RELATIONS_ONLY
984 983 end
985 984
986 985 # Add children to queue, but only if they are not already in it and
987 986 # the children of the current node need to be processed.
988 987 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
989 988 current_issue.children.each do |child|
990 989 next if except.include?(child)
991 990
992 991 if (issue_status[child] == eNOT_DISCOVERED)
993 992 queue << child
994 993 issue_status[child] = ePROCESS_ALL
995 994 elsif (issue_status[child] == eRELATIONS_PROCESSED)
996 995 queue << child
997 996 issue_status[child] = ePROCESS_CHILDREN_ONLY
998 997 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
999 998 queue << child
1000 999 issue_status[child] = ePROCESS_ALL
1001 1000 end
1002 1001 end
1003 1002 end
1004 1003
1005 1004 # Add related issues to the queue, if they are not already in it.
1006 1005 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1007 1006 next if except.include?(related_issue)
1008 1007
1009 1008 if (issue_status[related_issue] == eNOT_DISCOVERED)
1010 1009 queue << related_issue
1011 1010 issue_status[related_issue] = ePROCESS_ALL
1012 1011 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1013 1012 queue << related_issue
1014 1013 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1015 1014 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1016 1015 queue << related_issue
1017 1016 issue_status[related_issue] = ePROCESS_ALL
1018 1017 end
1019 1018 end
1020 1019
1021 1020 # Set new status for current issue
1022 1021 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1023 1022 issue_status[current_issue] = eALL_PROCESSED
1024 1023 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1025 1024 issue_status[current_issue] = eRELATIONS_PROCESSED
1026 1025 end
1027 1026 end # while
1028 1027
1029 1028 # Remove the issues from the "except" parameter from the result array
1030 1029 dependencies -= except
1031 1030 dependencies.delete(self)
1032 1031
1033 1032 dependencies
1034 1033 end
1035 1034
1036 1035 # Returns an array of issues that duplicate this one
1037 1036 def duplicates
1038 1037 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1039 1038 end
1040 1039
1041 1040 # Returns the due date or the target due date if any
1042 1041 # Used on gantt chart
1043 1042 def due_before
1044 1043 due_date || (fixed_version ? fixed_version.effective_date : nil)
1045 1044 end
1046 1045
1047 1046 # Returns the time scheduled for this issue.
1048 1047 #
1049 1048 # Example:
1050 1049 # Start Date: 2/26/09, End Date: 3/04/09
1051 1050 # duration => 6
1052 1051 def duration
1053 1052 (start_date && due_date) ? due_date - start_date : 0
1054 1053 end
1055 1054
1056 1055 # Returns the duration in working days
1057 1056 def working_duration
1058 1057 (start_date && due_date) ? working_days(start_date, due_date) : 0
1059 1058 end
1060 1059
1061 1060 def soonest_start(reload=false)
1062 1061 @soonest_start = nil if reload
1063 1062 @soonest_start ||= (
1064 1063 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1065 1064 [(@parent_issue || parent).try(:soonest_start)]
1066 1065 ).compact.max
1067 1066 end
1068 1067
1069 1068 # Sets start_date on the given date or the next working day
1070 1069 # and changes due_date to keep the same working duration.
1071 1070 def reschedule_on(date)
1072 1071 wd = working_duration
1073 1072 date = next_working_date(date)
1074 1073 self.start_date = date
1075 1074 self.due_date = add_working_days(date, wd)
1076 1075 end
1077 1076
1078 1077 # Reschedules the issue on the given date or the next working day and saves the record.
1079 1078 # If the issue is a parent task, this is done by rescheduling its subtasks.
1080 1079 def reschedule_on!(date)
1081 1080 return if date.nil?
1082 1081 if leaf?
1083 1082 if start_date.nil? || start_date != date
1084 1083 if start_date && start_date > date
1085 1084 # Issue can not be moved earlier than its soonest start date
1086 1085 date = [soonest_start(true), date].compact.max
1087 1086 end
1088 1087 reschedule_on(date)
1089 1088 begin
1090 1089 save
1091 1090 rescue ActiveRecord::StaleObjectError
1092 1091 reload
1093 1092 reschedule_on(date)
1094 1093 save
1095 1094 end
1096 1095 end
1097 1096 else
1098 1097 leaves.each do |leaf|
1099 1098 if leaf.start_date
1100 1099 # Only move subtask if it starts at the same date as the parent
1101 1100 # or if it starts before the given date
1102 1101 if start_date == leaf.start_date || date > leaf.start_date
1103 1102 leaf.reschedule_on!(date)
1104 1103 end
1105 1104 else
1106 1105 leaf.reschedule_on!(date)
1107 1106 end
1108 1107 end
1109 1108 end
1110 1109 end
1111 1110
1112 1111 def <=>(issue)
1113 1112 if issue.nil?
1114 1113 -1
1115 1114 elsif root_id != issue.root_id
1116 1115 (root_id || 0) <=> (issue.root_id || 0)
1117 1116 else
1118 1117 (lft || 0) <=> (issue.lft || 0)
1119 1118 end
1120 1119 end
1121 1120
1122 1121 def to_s
1123 1122 "#{tracker} ##{id}: #{subject}"
1124 1123 end
1125 1124
1126 1125 # Returns a string of css classes that apply to the issue
1127 1126 def css_classes(user=User.current)
1128 1127 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1129 1128 s << ' closed' if closed?
1130 1129 s << ' overdue' if overdue?
1131 1130 s << ' child' if child?
1132 1131 s << ' parent' unless leaf?
1133 1132 s << ' private' if is_private?
1134 1133 if user.logged?
1135 1134 s << ' created-by-me' if author_id == user.id
1136 1135 s << ' assigned-to-me' if assigned_to_id == user.id
1137 1136 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1138 1137 end
1139 1138 s
1140 1139 end
1141 1140
1142 1141 # Unassigns issues from +version+ if it's no longer shared with issue's project
1143 1142 def self.update_versions_from_sharing_change(version)
1144 1143 # Update issues assigned to the version
1145 1144 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1146 1145 end
1147 1146
1148 1147 # Unassigns issues from versions that are no longer shared
1149 1148 # after +project+ was moved
1150 1149 def self.update_versions_from_hierarchy_change(project)
1151 1150 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1152 1151 # Update issues of the moved projects and issues assigned to a version of a moved project
1153 1152 Issue.update_versions(
1154 1153 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1155 1154 moved_project_ids, moved_project_ids]
1156 1155 )
1157 1156 end
1158 1157
1159 1158 def parent_issue_id=(arg)
1160 1159 s = arg.to_s.strip.presence
1161 1160 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1162 1161 @invalid_parent_issue_id = nil
1163 1162 elsif s.blank?
1164 1163 @parent_issue = nil
1165 1164 @invalid_parent_issue_id = nil
1166 1165 else
1167 1166 @parent_issue = nil
1168 1167 @invalid_parent_issue_id = arg
1169 1168 end
1170 1169 end
1171 1170
1172 1171 def parent_issue_id
1173 1172 if @invalid_parent_issue_id
1174 1173 @invalid_parent_issue_id
1175 1174 elsif instance_variable_defined? :@parent_issue
1176 1175 @parent_issue.nil? ? nil : @parent_issue.id
1177 1176 else
1178 1177 parent_id
1179 1178 end
1180 1179 end
1181 1180
1182 1181 def set_parent_id
1183 1182 self.parent_id = parent_issue_id
1184 1183 end
1185 1184
1186 1185 # Returns true if issue's project is a valid
1187 1186 # parent issue project
1188 1187 def valid_parent_project?(issue=parent)
1189 1188 return true if issue.nil? || issue.project_id == project_id
1190 1189
1191 1190 case Setting.cross_project_subtasks
1192 1191 when 'system'
1193 1192 true
1194 1193 when 'tree'
1195 1194 issue.project.root == project.root
1196 1195 when 'hierarchy'
1197 1196 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1198 1197 when 'descendants'
1199 1198 issue.project.is_or_is_ancestor_of?(project)
1200 1199 else
1201 1200 false
1202 1201 end
1203 1202 end
1204 1203
1205 1204 # Returns an issue scope based on project and scope
1206 1205 def self.cross_project_scope(project, scope=nil)
1207 1206 if project.nil?
1208 1207 return Issue
1209 1208 end
1210 1209 case scope
1211 1210 when 'all', 'system'
1212 1211 Issue
1213 1212 when 'tree'
1214 1213 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1215 1214 :lft => project.root.lft, :rgt => project.root.rgt)
1216 1215 when 'hierarchy'
1217 1216 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1218 1217 :lft => project.lft, :rgt => project.rgt)
1219 1218 when 'descendants'
1220 1219 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1221 1220 :lft => project.lft, :rgt => project.rgt)
1222 1221 else
1223 1222 Issue.where(:project_id => project.id)
1224 1223 end
1225 1224 end
1226 1225
1227 1226 def self.by_tracker(project)
1228 1227 count_and_group_by(:project => project, :association => :tracker)
1229 1228 end
1230 1229
1231 1230 def self.by_version(project)
1232 1231 count_and_group_by(:project => project, :association => :fixed_version)
1233 1232 end
1234 1233
1235 1234 def self.by_priority(project)
1236 1235 count_and_group_by(:project => project, :association => :priority)
1237 1236 end
1238 1237
1239 1238 def self.by_category(project)
1240 1239 count_and_group_by(:project => project, :association => :category)
1241 1240 end
1242 1241
1243 1242 def self.by_assigned_to(project)
1244 1243 count_and_group_by(:project => project, :association => :assigned_to)
1245 1244 end
1246 1245
1247 1246 def self.by_author(project)
1248 1247 count_and_group_by(:project => project, :association => :author)
1249 1248 end
1250 1249
1251 1250 def self.by_subproject(project)
1252 1251 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1253 1252 r.reject {|r| r["project_id"] == project.id.to_s}
1254 1253 end
1255 1254
1256 1255 # Query generator for selecting groups of issue counts for a project
1257 1256 # based on specific criteria
1258 1257 #
1259 1258 # Options
1260 1259 # * project - Project to search in.
1261 1260 # * with_subprojects - Includes subprojects issues if set to true.
1262 1261 # * association - Symbol. Association for grouping.
1263 1262 def self.count_and_group_by(options)
1264 1263 assoc = reflect_on_association(options[:association])
1265 1264 select_field = assoc.foreign_key
1266 1265
1267 1266 Issue.
1268 1267 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1269 1268 joins(:status, assoc.name).
1270 1269 group(:status_id, :is_closed, select_field).
1271 1270 count.
1272 1271 map do |columns, total|
1273 1272 status_id, is_closed, field_value = columns
1274 1273 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1275 1274 {
1276 1275 "status_id" => status_id.to_s,
1277 1276 "closed" => is_closed,
1278 1277 select_field => field_value.to_s,
1279 1278 "total" => total.to_s
1280 1279 }
1281 1280 end
1282 1281 end
1283 1282
1284 1283 # Returns a scope of projects that user can assign the issue to
1285 1284 def allowed_target_projects(user=User.current)
1286 1285 if new_record?
1287 1286 Project.where(Project.allowed_to_condition(user, :add_issues))
1288 1287 else
1289 1288 self.class.allowed_target_projects_on_move(user)
1290 1289 end
1291 1290 end
1292 1291
1293 1292 # Returns a scope of projects that user can move issues to
1294 1293 def self.allowed_target_projects_on_move(user=User.current)
1295 1294 Project.where(Project.allowed_to_condition(user, :move_issues))
1296 1295 end
1297 1296
1298 1297 private
1299 1298
1300 1299 def after_project_change
1301 1300 # Update project_id on related time entries
1302 1301 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1303 1302
1304 1303 # Delete issue relations
1305 1304 unless Setting.cross_project_issue_relations?
1306 1305 relations_from.clear
1307 1306 relations_to.clear
1308 1307 end
1309 1308
1310 1309 # Move subtasks that were in the same project
1311 1310 children.each do |child|
1312 1311 next unless child.project_id == project_id_was
1313 1312 # Change project and keep project
1314 1313 child.send :project=, project, true
1315 1314 unless child.save
1316 1315 raise ActiveRecord::Rollback
1317 1316 end
1318 1317 end
1319 1318 end
1320 1319
1321 1320 # Callback for after the creation of an issue by copy
1322 1321 # * adds a "copied to" relation with the copied issue
1323 1322 # * copies subtasks from the copied issue
1324 1323 def after_create_from_copy
1325 1324 return unless copy? && !@after_create_from_copy_handled
1326 1325
1327 1326 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1328 1327 if @current_journal
1329 1328 @copied_from.init_journal(@current_journal.user)
1330 1329 end
1331 1330 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1332 1331 unless relation.save
1333 1332 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1334 1333 end
1335 1334 end
1336 1335
1337 1336 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1338 1337 copy_options = (@copy_options || {}).merge(:subtasks => false)
1339 1338 copied_issue_ids = {@copied_from.id => self.id}
1340 1339 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1341 1340 # Do not copy self when copying an issue as a descendant of the copied issue
1342 1341 next if child == self
1343 1342 # Do not copy subtasks of issues that were not copied
1344 1343 next unless copied_issue_ids[child.parent_id]
1345 1344 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1346 1345 unless child.visible?
1347 1346 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1348 1347 next
1349 1348 end
1350 1349 copy = Issue.new.copy_from(child, copy_options)
1351 1350 if @current_journal
1352 1351 copy.init_journal(@current_journal.user)
1353 1352 end
1354 1353 copy.author = author
1355 1354 copy.project = project
1356 1355 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1357 1356 unless copy.save
1358 1357 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1359 1358 next
1360 1359 end
1361 1360 copied_issue_ids[child.id] = copy.id
1362 1361 end
1363 1362 end
1364 1363 @after_create_from_copy_handled = true
1365 1364 end
1366 1365
1367 1366 def update_nested_set_attributes
1368 1367 if parent_id_changed?
1369 1368 update_nested_set_attributes_on_parent_change
1370 1369 end
1371 1370 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1372 1371 end
1373 1372
1374 1373 # Updates the nested set for when an existing issue is moved
1375 1374 def update_nested_set_attributes_on_parent_change
1376 1375 former_parent_id = parent_id_was
1377 1376 # delete invalid relations of all descendants
1378 1377 self_and_descendants.each do |issue|
1379 1378 issue.relations.each do |relation|
1380 1379 relation.destroy unless relation.valid?
1381 1380 end
1382 1381 end
1383 1382 # update former parent
1384 1383 recalculate_attributes_for(former_parent_id) if former_parent_id
1385 1384 end
1386 1385
1387 1386 def update_parent_attributes
1388 1387 if parent_id
1389 1388 recalculate_attributes_for(parent_id)
1390 1389 association(:parent).reset
1391 1390 end
1392 1391 end
1393 1392
1394 1393 def recalculate_attributes_for(issue_id)
1395 1394 if issue_id && p = Issue.find_by_id(issue_id)
1396 1395 # priority = highest priority of children
1397 1396 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1398 1397 p.priority = IssuePriority.find_by_position(priority_position)
1399 1398 end
1400 1399
1401 1400 # start/due dates = lowest/highest dates of children
1402 1401 p.start_date = p.children.minimum(:start_date)
1403 1402 p.due_date = p.children.maximum(:due_date)
1404 1403 if p.start_date && p.due_date && p.due_date < p.start_date
1405 1404 p.start_date, p.due_date = p.due_date, p.start_date
1406 1405 end
1407 1406
1408 1407 # done ratio = weighted average ratio of leaves
1409 1408 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1410 1409 leaves_count = p.leaves.count
1411 1410 if leaves_count > 0
1412 1411 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1413 1412 if average == 0
1414 1413 average = 1
1415 1414 end
1416 1415 done = p.leaves.joins(:status).
1417 1416 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1418 1417 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1419 1418 progress = done / (average * leaves_count)
1420 1419 p.done_ratio = progress.round
1421 1420 end
1422 1421 end
1423 1422
1424 1423 # estimate = sum of leaves estimates
1425 1424 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1426 1425 p.estimated_hours = nil if p.estimated_hours == 0.0
1427 1426
1428 1427 # ancestors will be recursively updated
1429 1428 p.save(:validate => false)
1430 1429 end
1431 1430 end
1432 1431
1433 1432 # Update issues so their versions are not pointing to a
1434 1433 # fixed_version that is not shared with the issue's project
1435 1434 def self.update_versions(conditions=nil)
1436 1435 # Only need to update issues with a fixed_version from
1437 1436 # a different project and that is not systemwide shared
1438 1437 Issue.joins(:project, :fixed_version).
1439 1438 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1440 1439 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1441 1440 " AND #{Version.table_name}.sharing <> 'system'").
1442 1441 where(conditions).each do |issue|
1443 1442 next if issue.project.nil? || issue.fixed_version.nil?
1444 1443 unless issue.project.shared_versions.include?(issue.fixed_version)
1445 1444 issue.init_journal(User.current)
1446 1445 issue.fixed_version = nil
1447 1446 issue.save
1448 1447 end
1449 1448 end
1450 1449 end
1451 1450
1452 1451 # Callback on file attachment
1453 1452 def attachment_added(attachment)
1454 1453 if current_journal && !attachment.new_record?
1455 1454 current_journal.journalize_attachment(attachment, :added)
1456 1455 end
1457 1456 end
1458 1457
1459 1458 # Callback on attachment deletion
1460 1459 def attachment_removed(attachment)
1461 1460 if current_journal && !attachment.new_record?
1462 1461 current_journal.journalize_attachment(attachment, :removed)
1463 1462 current_journal.save
1464 1463 end
1465 1464 end
1466 1465
1467 1466 # Called after a relation is added
1468 1467 def relation_added(relation)
1469 1468 if current_journal
1470 1469 current_journal.journalize_relation(relation, :added)
1471 1470 current_journal.save
1472 1471 end
1473 1472 end
1474 1473
1475 1474 # Called after a relation is removed
1476 1475 def relation_removed(relation)
1477 1476 if current_journal
1478 1477 current_journal.journalize_relation(relation, :removed)
1479 1478 current_journal.save
1480 1479 end
1481 1480 end
1482 1481
1483 1482 # Default assignment based on category
1484 1483 def default_assign
1485 1484 if assigned_to.nil? && category && category.assigned_to
1486 1485 self.assigned_to = category.assigned_to
1487 1486 end
1488 1487 end
1489 1488
1490 1489 # Updates start/due dates of following issues
1491 1490 def reschedule_following_issues
1492 1491 if start_date_changed? || due_date_changed?
1493 1492 relations_from.each do |relation|
1494 1493 relation.set_issue_to_dates
1495 1494 end
1496 1495 end
1497 1496 end
1498 1497
1499 1498 # Closes duplicates if the issue is being closed
1500 1499 def close_duplicates
1501 1500 if closing?
1502 1501 duplicates.each do |duplicate|
1503 1502 # Reload is needed in case the duplicate was updated by a previous duplicate
1504 1503 duplicate.reload
1505 1504 # Don't re-close it if it's already closed
1506 1505 next if duplicate.closed?
1507 1506 # Same user and notes
1508 1507 if @current_journal
1509 1508 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1510 1509 end
1511 1510 duplicate.update_attribute :status, self.status
1512 1511 end
1513 1512 end
1514 1513 end
1515 1514
1516 1515 # Make sure updated_on is updated when adding a note and set updated_on now
1517 1516 # so we can set closed_on with the same value on closing
1518 1517 def force_updated_on_change
1519 1518 if @current_journal || changed?
1520 1519 self.updated_on = current_time_from_proper_timezone
1521 1520 if new_record?
1522 1521 self.created_on = updated_on
1523 1522 end
1524 1523 end
1525 1524 end
1526 1525
1527 1526 # Callback for setting closed_on when the issue is closed.
1528 1527 # The closed_on attribute stores the time of the last closing
1529 1528 # and is preserved when the issue is reopened.
1530 1529 def update_closed_on
1531 1530 if closing?
1532 1531 self.closed_on = updated_on
1533 1532 end
1534 1533 end
1535 1534
1536 1535 # Saves the changes in a Journal
1537 1536 # Called after_save
1538 1537 def create_journal
1539 1538 if current_journal
1540 1539 current_journal.save
1541 1540 end
1542 1541 end
1543 1542
1544 1543 def send_notification
1545 1544 if Setting.notified_events.include?('issue_added')
1546 1545 Mailer.deliver_issue_add(self)
1547 1546 end
1548 1547 end
1549 1548
1550 1549 # Stores the previous assignee so we can still have access
1551 1550 # to it during after_save callbacks (assigned_to_id_was is reset)
1552 1551 def set_assigned_to_was
1553 1552 @previous_assigned_to_id = assigned_to_id_was
1554 1553 end
1555 1554
1556 1555 # Clears the previous assignee at the end of after_save callbacks
1557 1556 def clear_assigned_to_was
1558 1557 @assigned_to_was = nil
1559 1558 @previous_assigned_to_id = nil
1560 1559 end
1561 1560 end
@@ -1,48 +1,50
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 JournalDetail < ActiveRecord::Base
19 19 belongs_to :journal
20 before_save :normalize_values
21 20 attr_protected :id
22 21
23 22 def custom_field
24 23 if property == 'cf'
25 24 @custom_field ||= CustomField.find_by_id(prop_key)
26 25 end
27 26 end
28 27
29 private
28 def value=(arg)
29 write_attribute :value, normalize(arg)
30 end
30 31
31 def normalize_values
32 self.value = normalize(value)
33 self.old_value = normalize(old_value)
32 def old_value=(arg)
33 write_attribute :old_value, normalize(arg)
34 34 end
35 35
36 private
37
36 38 def normalize(v)
37 39 case v
38 40 when true
39 41 "1"
40 42 when false
41 43 "0"
42 44 when Date
43 45 v.strftime("%Y-%m-%d")
44 46 else
45 47 v
46 48 end
47 49 end
48 50 end
@@ -1,58 +1,58
1 1 <div class="contextual">
2 2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_user_plural)%></h2>
6 6
7 7 <%= form_tag(users_path, :method => :get) do %>
8 8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 9 <label for='status'><%= l(:field_status) %>:</label>
10 10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 11
12 12 <% if @groups.present? %>
13 13 <label for='group_id'><%= l(:label_group) %>:</label>
14 14 <%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
15 15 <% end %>
16 16
17 17 <label for='name'><%= l(:label_user) %>:</label>
18 18 <%= text_field_tag 'name', params[:name], :size => 30 %>
19 19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
20 20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
21 21 </fieldset>
22 22 <% end %>
23 23 &nbsp;
24 24
25 25 <div class="autoscroll">
26 26 <table class="list">
27 27 <thead><tr>
28 28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
29 29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
30 30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
31 31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
32 32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
33 33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
34 34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
35 35 <th></th>
36 36 </tr></thead>
37 37 <tbody>
38 38 <% for user in @users -%>
39 39 <tr class="<%= user.css_classes %> <%= cycle("odd", "even") %>">
40 40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
41 41 <td class="firstname"><%= h(user.firstname) %></td>
42 42 <td class="lastname"><%= h(user.lastname) %></td>
43 43 <td class="email"><%= mail_to(h(user.mail)) %></td>
44 44 <td class="tick"><%= checked_image user.admin? %></td>
45 45 <td class="created_on"><%= format_time(user.created_on) %></td>
46 46 <td class="last_login_on"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
47 47 <td class="buttons">
48 48 <%= change_status_link(user) %>
49 <%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %>
49 <%= delete_link user_path(user, :back_url => request.original_fullpath) unless User.current == user %>
50 50 </td>
51 51 </tr>
52 52 <% end -%>
53 53 </tbody>
54 54 </table>
55 55 </div>
56 56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
57 57
58 58 <% html_title(l(:label_user_plural)) -%>
@@ -1,33 +1,35
1 1 Rails.application.configure do
2 2 # Settings specified here will take precedence over those in config/application.rb
3 3
4 4 # The test environment is used exclusively to run your application's
5 5 # test suite. You never need to work with it otherwise. Remember that
6 6 # your test database is "scratch space" for the test suite and is wiped
7 7 # and recreated between test runs. Don't rely on the data there!
8 8 config.cache_classes = true
9 9
10 10 # Do not eager load code on boot. This avoids loading your whole application
11 11 # just for the purpose of running a single test. If you are using a tool that
12 12 # preloads Rails for running tests, you may have to set it to true.
13 13 config.eager_load = false
14 14
15 15 # Show full error reports and disable caching
16 16 config.consider_all_requests_local = true
17 17 config.action_controller.perform_caching = false
18 18
19 19 config.action_mailer.perform_deliveries = true
20 20
21 21 # Tell Action Mailer not to deliver emails to the real world.
22 22 # The :test delivery method accumulates sent emails in the
23 23 # ActionMailer::Base.deliveries array.
24 24 config.action_mailer.delivery_method = :test
25 25
26 26 # Disable request forgery protection in test environment.
27 27 config.action_controller.allow_forgery_protection = false
28 28
29 29 # Print deprecation notices to stderr and the Rails logger.
30 30 config.active_support.deprecation = [:stderr, :log]
31 31
32 config.secret_token = 'a secret token for running the tests'
32 config.secret_key_base = 'a secret token for running the tests'
33
34 config.active_support.test_order = :random
33 35 end
@@ -1,194 +1,180
1 1 require 'active_record'
2 2
3 3 module ActiveRecord
4 4 class Base
5 5 include Redmine::I18n
6 6 # Translate attribute names for validation errors display
7 7 def self.human_attribute_name(attr, *args)
8 8 attr = attr.to_s.sub(/_id$/, '').sub(/^.+\./, '')
9 9 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
10 10 end
11 11 end
12 12
13 13 # Undefines private Kernel#open method to allow using `open` scopes in models.
14 14 # See Defect #11545 (http://www.redmine.org/issues/11545) for details.
15 15 class Base
16 16 class << self
17 17 undef open
18 18 end
19 19 end
20 20 class Relation ; undef open ; end
21 21 end
22 22
23 23 module ActionView
24 24 module Helpers
25 25 module DateHelper
26 26 # distance_of_time_in_words breaks when difference is greater than 30 years
27 27 def distance_of_date_in_words(from_date, to_date = 0, options = {})
28 28 from_date = from_date.to_date if from_date.respond_to?(:to_date)
29 29 to_date = to_date.to_date if to_date.respond_to?(:to_date)
30 30 distance_in_days = (to_date - from_date).abs
31 31
32 32 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
33 33 case distance_in_days
34 34 when 0..60 then locale.t :x_days, :count => distance_in_days.round
35 35 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
36 36 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
37 37 end
38 38 end
39 39 end
40 40 end
41 41 end
42 42
43 43 class Resolver
44 44 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
45 45 cached(key, [name, prefix, partial], details, locals) do
46 46 if details[:formats] & [:xml, :json]
47 47 details = details.dup
48 48 details[:formats] = details[:formats].dup + [:api]
49 49 end
50 50 find_templates(name, prefix, partial, details)
51 51 end
52 52 end
53 53 end
54 54 end
55 55
56 56 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
57 57
58 58 # HTML5: <option value=""></option> is invalid, use <option value="">&nbsp;</option> instead
59 59 module ActionView
60 60 module Helpers
61 61 module Tags
62 62 class Base
63 63 private
64 64 def add_options_with_non_empty_blank_option(option_tags, options, value = nil)
65 65 if options[:include_blank] == true
66 66 options = options.dup
67 67 options[:include_blank] = '&nbsp;'.html_safe
68 68 end
69 69 add_options_without_non_empty_blank_option(option_tags, options, value)
70 70 end
71 71 alias_method_chain :add_options, :non_empty_blank_option
72 72 end
73 73 end
74 74
75 75 module FormTagHelper
76 76 def select_tag_with_non_empty_blank_option(name, option_tags = nil, options = {})
77 77 if options.delete(:include_blank)
78 78 options[:prompt] = '&nbsp;'.html_safe
79 79 end
80 80 select_tag_without_non_empty_blank_option(name, option_tags, options)
81 81 end
82 82 alias_method_chain :select_tag, :non_empty_blank_option
83 83 end
84 84
85 85 module FormOptionsHelper
86 86 def options_for_select_with_non_empty_blank_option(container, selected = nil)
87 87 if container.is_a?(Array)
88 88 container = container.map {|element| element.blank? ? ["&nbsp;".html_safe, ""] : element}
89 89 end
90 90 options_for_select_without_non_empty_blank_option(container, selected)
91 91 end
92 92 alias_method_chain :options_for_select, :non_empty_blank_option
93 93 end
94 94 end
95 95 end
96 96
97 97 require 'mail'
98 98
99 99 module DeliveryMethods
100 100 class AsyncSMTP < ::Mail::SMTP
101 101 def deliver!(*args)
102 102 Thread.start do
103 103 super *args
104 104 end
105 105 end
106 106 end
107 107
108 108 class AsyncSendmail < ::Mail::Sendmail
109 109 def deliver!(*args)
110 110 Thread.start do
111 111 super *args
112 112 end
113 113 end
114 114 end
115 115
116 116 class TmpFile
117 117 def initialize(*args); end
118 118
119 119 def deliver!(mail)
120 120 dest_dir = File.join(Rails.root, 'tmp', 'emails')
121 121 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
122 122 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
123 123 end
124 124 end
125 125 end
126 126
127 127 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
128 128 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
129 129 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
130 130
131 131 # Changes how sent emails are logged
132 132 # Rails doesn't log cc and bcc which is misleading when using bcc only (#12090)
133 133 module ActionMailer
134 134 class LogSubscriber < ActiveSupport::LogSubscriber
135 135 def deliver(event)
136 136 recipients = [:to, :cc, :bcc].inject("") do |s, header|
137 137 r = Array.wrap(event.payload[header])
138 138 if r.any?
139 139 s << "\n #{header}: #{r.join(', ')}"
140 140 end
141 141 s
142 142 end
143 143 info("\nSent email \"#{event.payload[:subject]}\" (%1.fms)#{recipients}" % event.duration)
144 144 debug(event.payload[:mail])
145 145 end
146 146 end
147 147 end
148 148
149 # #deliver is deprecated in Rails 4.2
150 # Prevents massive deprecation warnings
151 module ActionMailer
152 class MessageDelivery < Delegator
153 def deliver
154 deliver_now
155 end
156 end
157 end
158
149 159 module ActionController
150 160 module MimeResponds
151 161 class Collector
152 162 def api(&block)
153 163 any(:xml, :json, &block)
154 164 end
155 165 end
156 166 end
157 167 end
158 168
159 169 module ActionController
160 170 class Base
161 171 # Displays an explicit message instead of a NoMethodError exception
162 172 # when trying to start Redmine with an old session_store.rb
163 173 # TODO: remove it in a later version
164 174 def self.session=(*args)
165 175 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
166 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
176 "Setting the session secret with ActionController.session= is no longer supported."
167 177 exit 1
168 178 end
169 179 end
170 180 end
171
172 if Rails::VERSION::MAJOR < 4 && RUBY_VERSION >= "2.1"
173 module ActiveSupport
174 class HashWithIndifferentAccess
175 def select(*args, &block)
176 dup.tap { |hash| hash.select!(*args, &block) }
177 end
178
179 def reject(*args, &block)
180 dup.tap { |hash| hash.reject!(*args, &block) }
181 end
182 end
183
184 class OrderedHash
185 def select(*args, &block)
186 dup.tap { |hash| hash.select!(*args, &block) }
187 end
188
189 def reject(*args, &block)
190 dup.tap { |hash| hash.reject!(*args, &block) }
191 end
192 end
193 end
194 end
@@ -1,351 +1,355
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 Rails.application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
27 27
28 28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
29 29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
30 30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
31 31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
32 32
33 33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
34 34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
35 35
36 36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
37 37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
38 38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
39 39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
40 40
41 41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
42 42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
43 43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
44 44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
45 45
46 46 # Misc issue routes. TODO: move into resources
47 47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
48 48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
49 49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
50 50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
51 51
52 52 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
53 53 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
54 54
55 55 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
56 56 get '/issues/gantt', :to => 'gantts#show'
57 57
58 58 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
59 59 get '/issues/calendar', :to => 'calendars#show'
60 60
61 61 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
62 62 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
63 63
64 64 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
65 65 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
66 66 match 'my/page', :controller => 'my', :action => 'page', :via => :get
67 67 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
68 68 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
69 69 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
70 70 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
71 71 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
72 72 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
73 73 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
74 74 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
75 75
76 76 resources :users do
77 77 resources :memberships, :controller => 'principal_memberships'
78 78 resources :email_addresses, :only => [:index, :create, :update, :destroy]
79 79 end
80 80
81 81 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
82 82 delete 'watchers/watch', :to => 'watchers#unwatch'
83 83 get 'watchers/new', :to => 'watchers#new'
84 84 post 'watchers', :to => 'watchers#create'
85 85 post 'watchers/append', :to => 'watchers#append'
86 86 delete 'watchers', :to => 'watchers#destroy'
87 87 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
88 88 # Specific routes for issue watchers API
89 89 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
90 90 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
91 91
92 92 resources :projects do
93 93 member do
94 94 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
95 95 post 'modules'
96 96 post 'archive'
97 97 post 'unarchive'
98 98 post 'close'
99 99 post 'reopen'
100 100 match 'copy', :via => [:get, :post]
101 101 end
102 102
103 103 shallow do
104 104 resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
105 105 collection do
106 106 get 'autocomplete'
107 107 end
108 108 end
109 109 end
110 110
111 111 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
112 112
113 113 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
114 114 resources :issues, :only => [:index, :new, :create]
115 115 # issue form update
116 116 match 'issues/update_form', :controller => 'issues', :action => 'update_form', :via => [:put, :patch, :post], :as => 'issue_form'
117 117
118 118 resources :files, :only => [:index, :new, :create]
119 119
120 120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
121 121 collection do
122 122 put 'close_completed'
123 123 end
124 124 end
125 125 get 'versions.:format', :to => 'versions#index'
126 126 get 'roadmap', :to => 'versions#index', :format => false
127 127 get 'versions', :to => 'versions#index'
128 128
129 129 resources :news, :except => [:show, :edit, :update, :destroy]
130 130 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
131 131 get 'report', :on => :collection
132 132 end
133 133 resources :queries, :only => [:new, :create]
134 134 shallow do
135 135 resources :issue_categories
136 136 end
137 137 resources :documents, :except => [:show, :edit, :update, :destroy]
138 138 resources :boards
139 139 shallow do
140 140 resources :repositories, :except => [:index, :show] do
141 141 member do
142 142 match 'committers', :via => [:get, :post]
143 143 end
144 144 end
145 145 end
146 146
147 147 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
148 148 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
149 149 member do
150 150 get 'rename'
151 151 post 'rename'
152 152 get 'history'
153 153 get 'diff'
154 154 match 'preview', :via => [:post, :put, :patch]
155 155 post 'protect'
156 156 post 'add_attachment'
157 157 end
158 158 collection do
159 159 get 'export'
160 160 get 'date_index'
161 161 end
162 162 end
163 163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
164 164 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
165 165 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
166 166 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
167 167 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
168 168 end
169 169
170 170 resources :issues do
171 171 collection do
172 172 match 'bulk_edit', :via => [:get, :post]
173 173 post 'bulk_update'
174 174 end
175 175 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
176 176 collection do
177 177 get 'report'
178 178 end
179 179 end
180 180 shallow do
181 181 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
182 182 end
183 183 end
184 184 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
185 185
186 186 resources :queries, :except => [:show]
187 187
188 188 resources :news, :only => [:index, :show, :edit, :update, :destroy]
189 189 match '/news/:id/comments', :to => 'comments#create', :via => :post
190 190 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
191 191
192 192 resources :versions, :only => [:show, :edit, :update, :destroy] do
193 193 post 'status_by', :on => :member
194 194 end
195 195
196 196 resources :documents, :only => [:show, :edit, :update, :destroy] do
197 197 post 'add_attachment', :on => :member
198 198 end
199 199
200 200 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
201 201
202 202 resources :time_entries, :controller => 'timelog', :except => :destroy do
203 203 collection do
204 204 get 'report'
205 205 get 'bulk_edit'
206 206 post 'bulk_update'
207 207 end
208 208 end
209 209 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
210 210 # TODO: delete /time_entries for bulk deletion
211 211 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
212 212
213 213 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
214 214 get 'activity', :to => 'activities#index'
215 215
216 216 # repositories routes
217 217 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
218 218 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
219 219
220 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
221 :to => 'repositories#changes'
220 get 'projects/:id/repository/:repository_id/changes(/*path)',
221 :to => 'repositories#changes',
222 :format => false
222 223
223 224 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
224 225 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
225 226 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
226 227 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
227 228 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
228 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
229 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path)',
229 230 :controller => 'repositories',
230 231 :format => false,
231 232 :constraints => {
232 233 :action => /(browse|show|entry|raw|annotate|diff)/,
233 234 :rev => /[a-z0-9\.\-_]+/
234 235 }
235 236
236 237 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
237 238 get 'projects/:id/repository/graph', :to => 'repositories#graph'
238 239
239 get 'projects/:id/repository/changes(/*path(.:ext))',
240 :to => 'repositories#changes'
240 get 'projects/:id/repository/changes(/*path)',
241 :to => 'repositories#changes',
242 :format => false
241 243
242 244 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
243 245 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
244 246 get 'projects/:id/repository/revision', :to => 'repositories#revision'
245 247 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
246 248 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
247 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
249 get 'projects/:id/repository/revisions/:rev/:action(/*path)',
248 250 :controller => 'repositories',
249 251 :format => false,
250 252 :constraints => {
251 253 :action => /(browse|show|entry|raw|annotate|diff)/,
252 254 :rev => /[a-z0-9\.\-_]+/
253 255 }
254 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
256 get 'projects/:id/repository/:repository_id/:action(/*path)',
255 257 :controller => 'repositories',
256 :action => /(browse|show|entry|raw|changes|annotate|diff)/
257 get 'projects/:id/repository/:action(/*path(.:ext))',
258 :action => /(browse|show|entry|raw|changes|annotate|diff)/,
259 :format => false
260 get 'projects/:id/repository/:action(/*path)',
258 261 :controller => 'repositories',
259 :action => /(browse|show|entry|raw|changes|annotate|diff)/
262 :action => /(browse|show|entry|raw|changes|annotate|diff)/,
263 :format => false
260 264
261 265 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
262 266 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
263 267
264 268 # additional routes for having the file name at the end of url
265 269 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
266 270 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
267 271 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
268 272 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
269 273 resources :attachments, :only => [:show, :destroy]
270 274 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit
271 275 patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments
272 276
273 277 resources :groups do
274 278 resources :memberships, :controller => 'principal_memberships'
275 279 member do
276 280 get 'autocomplete_for_user'
277 281 end
278 282 end
279 283
280 284 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
281 285 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
282 286 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
283 287
284 288 resources :trackers, :except => :show do
285 289 collection do
286 290 match 'fields', :via => [:get, :post]
287 291 end
288 292 end
289 293 resources :issue_statuses, :except => :show do
290 294 collection do
291 295 post 'update_issue_done_ratio'
292 296 end
293 297 end
294 298 resources :custom_fields, :except => :show
295 299 resources :roles do
296 300 collection do
297 301 match 'permissions', :via => [:get, :post]
298 302 end
299 303 end
300 304 resources :enumerations, :except => :show
301 305 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
302 306
303 307 get 'projects/:id/search', :controller => 'search', :action => 'index'
304 308 get 'search', :controller => 'search', :action => 'index'
305 309
306 310 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
307 311
308 312 match 'admin', :controller => 'admin', :action => 'index', :via => :get
309 313 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
310 314 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
311 315 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
312 316 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
313 317 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
314 318
315 319 resources :auth_sources do
316 320 member do
317 321 get 'test_connection', :as => 'try_connection'
318 322 end
319 323 collection do
320 324 get 'autocomplete_for_new_user'
321 325 end
322 326 end
323 327
324 328 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
325 329 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
326 330 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
327 331 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
328 332 match 'settings', :controller => 'settings', :action => 'index', :via => :get
329 333 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
330 334 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
331 335
332 336 match 'sys/projects', :to => 'sys#projects', :via => :get
333 337 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
334 338 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
335 339
336 340 match 'uploads', :to => 'attachments#upload', :via => :post
337 341
338 342 get 'robots.txt', :to => 'welcome#robots'
339 343
340 344 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
341 345 file = File.join(plugin_dir, "config/routes.rb")
342 346 if File.exists?(file)
343 347 begin
344 348 instance_eval File.read(file)
345 349 rescue Exception => e
346 350 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
347 351 exit 1
348 352 end
349 353 end
350 354 end
351 355 end
@@ -1,24 +1,24
1 1 desc 'Generates a secret token for the application.'
2 2
3 3 file 'config/initializers/secret_token.rb' do
4 4 path = File.join(Rails.root, 'config', 'initializers', 'secret_token.rb')
5 5 secret = SecureRandom.hex(40)
6 6 File.open(path, 'w') do |f|
7 7 f.write <<"EOF"
8 8 # This file was generated by 'rake generate_secret_token', and should
9 9 # not be made visible to public.
10 10 # If you have a load-balancing Redmine cluster, you will need to use the
11 11 # same version of this file on each machine. And be sure to restart your
12 12 # server when you modify this file.
13 13 #
14 14 # Your secret key for verifying cookie session data integrity. If you
15 15 # change this key, all old sessions will become invalid! Make sure the
16 16 # secret is at least 30 characters and all random, no regular words or
17 17 # you'll be exposed to dictionary attacks.
18 RedmineApp::Application.config.secret_token = '#{secret}'
18 RedmineApp::Application.config.secret_key_base = '#{secret}'
19 19 EOF
20 20 end
21 21 end
22 22
23 23 desc 'Generates a secret token for the application.'
24 24 task :generate_secret_token => ['config/initializers/secret_token.rb']
@@ -1,110 +1,110
1 1 namespace :test do
2 2 desc 'Measures test coverage'
3 3 task :coverage do
4 4 rm_f "coverage"
5 5 ENV["COVERAGE"] = "1"
6 6 Rake::Task["test"].invoke
7 7 end
8 8
9 9 desc 'Run unit and functional scm tests'
10 10 task :scm do
11 11 errors = %w(test:scm:units test:scm:functionals).collect do |task|
12 12 begin
13 13 Rake::Task[task].invoke
14 14 nil
15 15 rescue => e
16 16 task
17 17 end
18 18 end.compact
19 19 abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any?
20 20 end
21 21
22 22 namespace :scm do
23 23 namespace :setup do
24 24 desc "Creates directory for test repositories"
25 25 task :create_dir => :environment do
26 26 FileUtils.mkdir_p Rails.root + '/tmp/test'
27 27 end
28 28
29 29 supported_scms = [:subversion, :cvs, :bazaar, :mercurial, :git, :darcs, :filesystem]
30 30
31 31 desc "Creates a test subversion repository"
32 32 task :subversion => :create_dir do
33 33 repo_path = "tmp/test/subversion_repository"
34 34 unless File.exists?(repo_path)
35 35 system "svnadmin create #{repo_path}"
36 36 system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}"
37 37 end
38 38 end
39 39
40 40 desc "Creates a test mercurial repository"
41 41 task :mercurial => :create_dir do
42 42 repo_path = "tmp/test/mercurial_repository"
43 43 unless File.exists?(repo_path)
44 44 bundle_path = "test/fixtures/repositories/mercurial_repository.hg"
45 45 system "hg init #{repo_path}"
46 46 system "hg -R #{repo_path} pull #{bundle_path}"
47 47 end
48 48 end
49 49
50 50 def extract_tar_gz(prefix)
51 51 unless File.exists?("tmp/test/#{prefix}_repository")
52 52 # system "gunzip < test/fixtures/repositories/#{prefix}_repository.tar.gz | tar -xv -C tmp/test"
53 53 system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{prefix}_repository.tar.gz"
54 54 end
55 55 end
56 56
57 57 (supported_scms - [:subversion, :mercurial]).each do |scm|
58 58 desc "Creates a test #{scm} repository"
59 59 task scm => :create_dir do
60 60 extract_tar_gz(scm)
61 61 end
62 62 end
63 63
64 64 desc "Creates all test repositories"
65 65 task :all => supported_scms
66 66 end
67 67
68 68 desc "Updates installed test repositories"
69 69 task :update => :environment do
70 70 require 'fileutils'
71 71 Dir.glob("tmp/test/*_repository").each do |dir|
72 72 next unless File.basename(dir) =~ %r{^(.+)_repository$} && File.directory?(dir)
73 73 scm = $1
74 74 next unless fixture = Dir.glob("test/fixtures/repositories/#{scm}_repository.*").first
75 75 next if File.stat(dir).ctime > File.stat(fixture).mtime
76 76
77 77 FileUtils.rm_rf dir
78 78 Rake::Task["test:scm:setup:#{scm}"].execute
79 79 end
80 80 end
81 81
82 82 Rake::TestTask.new(:units => "db:test:prepare") do |t|
83 83 t.libs << "test"
84 84 t.verbose = true
85 85 t.test_files = FileList['test/unit/repository*_test.rb'] + FileList['test/unit/lib/redmine/scm/**/*_test.rb']
86 86 end
87 87 Rake::Task['test:scm:units'].comment = "Run the scm unit tests"
88 88
89 89 Rake::TestTask.new(:functionals => "db:test:prepare") do |t|
90 90 t.libs << "test"
91 91 t.verbose = true
92 92 t.test_files = FileList['test/functional/repositories*_test.rb']
93 93 end
94 94 Rake::Task['test:scm:functionals'].comment = "Run the scm functional tests"
95 95 end
96 96
97 97 Rake::TestTask.new(:routing) do |t|
98 98 t.libs << "test"
99 99 t.verbose = true
100 100 t.test_files = FileList['test/integration/routing/*_test.rb'] + FileList['test/integration/api_test/*_routing_test.rb']
101 101 end
102 102 Rake::Task['test:routing'].comment = "Run the routing tests"
103 103
104 104 Rake::TestTask.new(:ui => "db:test:prepare") do |t|
105 105 t.libs << "test"
106 106 t.verbose = true
107 t.test_files = FileList['test/ui/**/*_test.rb']
107 t.test_files = FileList['test/ui/**/*_test_ui.rb']
108 108 end
109 109 Rake::Task['test:ui'].comment = "Run the UI tests with Capybara (PhantomJS listening on port 4444 is required)"
110 110 end
1 NO CONTENT: file renamed from test/extra/redmine_pm/repository_git_test.rb to test/extra/redmine_pm/repository_git_test_pm.rb
1 NO CONTENT: file renamed from test/extra/redmine_pm/repository_subversion_test.rb to test/extra/redmine_pm/repository_subversion_test_pm.rb
@@ -1,160 +1,160
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ActivitiesControllerTest < ActionController::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :enumerations, :users, :issue_categories,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :groups_users,
28 28 :enabled_modules,
29 29 :journals, :journal_details
30 30
31 31
32 32 def test_project_index
33 33 get :index, :id => 1, :with_subprojects => 0
34 34 assert_response :success
35 35 assert_template 'index'
36 36 assert_not_nil assigns(:events_by_day)
37 37
38 38 assert_select 'h3', :text => /#{2.days.ago.to_date.day}/
39 39 assert_select 'dl dt.issue-edit a', :text => /(#{IssueStatus.find(2).name})/
40 40 end
41 41
42 42 def test_project_index_with_invalid_project_id_should_respond_404
43 43 get :index, :id => 299
44 44 assert_response 404
45 45 end
46 46
47 47 def test_previous_project_index
48 48 get :index, :id => 1, :from => 2.days.ago.to_date
49 49 assert_response :success
50 50 assert_template 'index'
51 51 assert_not_nil assigns(:events_by_day)
52 52
53 53 assert_select 'h3', :text => /#{3.days.ago.to_date.day}/
54 54 assert_select 'dl dt.issue a', :text => /Cannot print recipes/
55 55 end
56 56
57 57 def test_global_index
58 58 @request.session[:user_id] = 1
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index'
62 62 assert_not_nil assigns(:events_by_day)
63 63
64 64 i5 = Issue.find(5)
65 65 d5 = User.find(1).time_to_date(i5.created_on)
66 66
67 67 assert_select 'h3', :text => /#{d5.day}/
68 68 assert_select 'dl dt.issue a', :text => /Subproject issue/
69 69 end
70 70
71 71 def test_user_index
72 72 @request.session[:user_id] = 1
73 73 get :index, :user_id => 2
74 74 assert_response :success
75 75 assert_template 'index'
76 76 assert_not_nil assigns(:events_by_day)
77 77
78 78 assert_select 'h2 a[href="/users/2"]', :text => 'John Smith'
79 79
80 80 i1 = Issue.find(1)
81 81 d1 = User.find(1).time_to_date(i1.created_on)
82 82
83 83 assert_select 'h3', :text => /#{d1.day}/
84 84 assert_select 'dl dt.issue a', :text => /Cannot print recipes/
85 85 end
86 86
87 87 def test_user_index_with_invalid_user_id_should_respond_404
88 88 get :index, :user_id => 299
89 89 assert_response 404
90 90 end
91 91
92 92 def test_index_atom_feed
93 93 get :index, :format => 'atom', :with_subprojects => 0
94 94 assert_response :success
95 95 assert_template 'common/feed'
96 96
97 97 assert_select 'feed' do
98 98 assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?with_subprojects=0'
99 99 assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?with_subprojects=0'
100 100 assert_select 'entry' do
101 101 assert_select 'link[href=?]', 'http://test.host/issues/11'
102 102 end
103 103 end
104 104 end
105 105
106 106 def test_index_atom_feed_with_explicit_selection
107 107 get :index, :format => 'atom', :with_subprojects => 0,
108 108 :show_changesets => 1,
109 109 :show_documents => 1,
110 110 :show_files => 1,
111 111 :show_issues => 1,
112 112 :show_messages => 1,
113 113 :show_news => 1,
114 114 :show_time_entries => 1,
115 115 :show_wiki_edits => 1
116 116
117 117 assert_response :success
118 118 assert_template 'common/feed'
119 119
120 120 assert_select 'feed' do
121 assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?show_changesets=1&amp;show_documents=1&amp;show_files=1&amp;show_issues=1&amp;show_messages=1&amp;show_news=1&amp;show_time_entries=1&amp;show_wiki_edits=1&amp;with_subprojects=0'
122 assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?show_changesets=1&amp;show_documents=1&amp;show_files=1&amp;show_issues=1&amp;show_messages=1&amp;show_news=1&amp;show_time_entries=1&amp;show_wiki_edits=1&amp;with_subprojects=0'
121 assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'
122 assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'
123 123 assert_select 'entry' do
124 124 assert_select 'link[href=?]', 'http://test.host/issues/11'
125 125 end
126 126 end
127 127 end
128 128
129 129 def test_index_atom_feed_with_one_item_type
130 130 with_settings :default_language => 'en' do
131 131 get :index, :format => 'atom', :show_issues => '1'
132 132 assert_response :success
133 133 assert_template 'common/feed'
134 134
135 135 assert_select 'title', :text => /Issues/
136 136 end
137 137 end
138 138
139 139 def test_index_atom_feed_with_user
140 140 get :index, :user_id => 2, :format => 'atom'
141 141
142 142 assert_response :success
143 143 assert_template 'common/feed'
144 144 assert_select 'title', :text => "Redmine: #{User.find(2).name}"
145 145 end
146 146
147 147 def test_index_should_show_private_notes_with_permission_only
148 148 journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true)
149 149 @request.session[:user_id] = 2
150 150
151 151 get :index
152 152 assert_response :success
153 153 assert_include journal, assigns(:events_by_day).values.flatten
154 154
155 155 Role.find(1).remove_permission! :view_private_notes
156 156 get :index
157 157 assert_response :success
158 158 assert_not_include journal, assigns(:events_by_day).values.flatten
159 159 end
160 160 end
@@ -1,173 +1,173
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class AuthSourcesControllerTest < ActionController::TestCase
21 21 fixtures :users, :auth_sources
22 22
23 23 def setup
24 24 @request.session[:user_id] = 1
25 25 end
26 26
27 27 def test_index
28 28 get :index
29 29
30 30 assert_response :success
31 31 assert_template 'index'
32 32 assert_not_nil assigns(:auth_sources)
33 33 end
34 34
35 35 def test_new
36 36 get :new
37 37
38 38 assert_response :success
39 39 assert_template 'new'
40 40
41 41 source = assigns(:auth_source)
42 42 assert_equal AuthSourceLdap, source.class
43 43 assert source.new_record?
44 44
45 45 assert_select 'form#auth_source_form' do
46 46 assert_select 'input[name=type][value=AuthSourceLdap]'
47 47 assert_select 'input[name=?]', 'auth_source[host]'
48 48 end
49 49 end
50 50
51 51 def test_new_with_invalid_type_should_respond_with_404
52 52 get :new, :type => 'foo'
53 53 assert_response 404
54 54 end
55 55
56 56 def test_create
57 57 assert_difference 'AuthSourceLdap.count' do
58 58 post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
59 59 assert_redirected_to '/auth_sources'
60 60 end
61 61
62 62 source = AuthSourceLdap.order('id DESC').first
63 63 assert_equal 'Test', source.name
64 64 assert_equal '127.0.0.1', source.host
65 65 assert_equal 389, source.port
66 66 assert_equal 'cn', source.attr_login
67 67 end
68 68
69 69 def test_create_with_failure
70 70 assert_no_difference 'AuthSourceLdap.count' do
71 71 post :create, :type => 'AuthSourceLdap',
72 72 :auth_source => {:name => 'Test', :host => '',
73 73 :port => '389', :attr_login => 'cn'}
74 74 assert_response :success
75 75 assert_template 'new'
76 76 end
77 77 assert_select_error /host cannot be blank/i
78 78 end
79 79
80 80 def test_edit
81 81 get :edit, :id => 1
82 82
83 83 assert_response :success
84 84 assert_template 'edit'
85 85
86 86 assert_select 'form#auth_source_form' do
87 87 assert_select 'input[name=?]', 'auth_source[host]'
88 88 end
89 89 end
90 90
91 91 def test_edit_should_not_contain_password
92 92 AuthSource.find(1).update_column :account_password, 'secret'
93 93
94 94 get :edit, :id => 1
95 95 assert_response :success
96 96 assert_select 'input[value=secret]', 0
97 assert_select 'input[name=dummy_password][value=?]', /x+/
97 assert_select 'input[name=dummy_password][value^=xxxxxx]'
98 98 end
99 99
100 100 def test_edit_invalid_should_respond_with_404
101 101 get :edit, :id => 99
102 102 assert_response 404
103 103 end
104 104
105 105 def test_update
106 106 put :update, :id => 1,
107 107 :auth_source => {:name => 'Renamed', :host => '192.168.0.10',
108 108 :port => '389', :attr_login => 'uid'}
109 109 assert_redirected_to '/auth_sources'
110 110 source = AuthSourceLdap.find(1)
111 111 assert_equal 'Renamed', source.name
112 112 assert_equal '192.168.0.10', source.host
113 113 end
114 114
115 115 def test_update_with_failure
116 116 put :update, :id => 1,
117 117 :auth_source => {:name => 'Renamed', :host => '',
118 118 :port => '389', :attr_login => 'uid'}
119 119 assert_response :success
120 120 assert_template 'edit'
121 121 assert_select_error /host cannot be blank/i
122 122 end
123 123
124 124 def test_destroy
125 125 assert_difference 'AuthSourceLdap.count', -1 do
126 126 delete :destroy, :id => 1
127 127 assert_redirected_to '/auth_sources'
128 128 end
129 129 end
130 130
131 131 def test_destroy_auth_source_in_use
132 132 User.find(2).update_attribute :auth_source_id, 1
133 133
134 134 assert_no_difference 'AuthSourceLdap.count' do
135 135 delete :destroy, :id => 1
136 136 assert_redirected_to '/auth_sources'
137 137 end
138 138 end
139 139
140 140 def test_test_connection
141 141 AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
142 142
143 143 get :test_connection, :id => 1
144 144 assert_redirected_to '/auth_sources'
145 145 assert_not_nil flash[:notice]
146 146 assert_match /successful/i, flash[:notice]
147 147 end
148 148
149 149 def test_test_connection_with_failure
150 150 AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
151 151
152 152 get :test_connection, :id => 1
153 153 assert_redirected_to '/auth_sources'
154 154 assert_not_nil flash[:error]
155 155 assert_include 'Something went wrong', flash[:error]
156 156 end
157 157
158 158 def test_autocomplete_for_new_user
159 159 AuthSource.expects(:search).with('foo').returns([
160 160 {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1},
161 161 {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1}
162 162 ])
163 163
164 164 get :autocomplete_for_new_user, :term => 'foo'
165 165 assert_response :success
166 166 assert_equal 'application/json', response.content_type
167 167 json = ActiveSupport::JSON.decode(response.body)
168 168 assert_kind_of Array, json
169 169 assert_equal 2, json.size
170 170 assert_equal 'foo1', json.first['value']
171 171 assert_equal 'foo1 (John Smith)', json.first['label']
172 172 end
173 173 end
@@ -1,217 +1,221
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class BoardsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
22 22
23 23 def setup
24 24 User.current = nil
25 25 end
26 26
27 27 def test_index
28 28 get :index, :project_id => 1
29 29 assert_response :success
30 30 assert_template 'index'
31 31 assert_not_nil assigns(:boards)
32 32 assert_not_nil assigns(:project)
33 33 end
34 34
35 35 def test_index_not_found
36 36 get :index, :project_id => 97
37 37 assert_response 404
38 38 end
39 39
40 40 def test_index_should_show_messages_if_only_one_board
41 41 Project.find(1).boards.slice(1..-1).each(&:destroy)
42 42
43 43 get :index, :project_id => 1
44 44 assert_response :success
45 45 assert_template 'show'
46 46 assert_not_nil assigns(:topics)
47 47 end
48 48
49 49 def test_show
50 50 get :show, :project_id => 1, :id => 1
51 51 assert_response :success
52 52 assert_template 'show'
53 53 assert_not_nil assigns(:board)
54 54 assert_not_nil assigns(:project)
55 55 assert_not_nil assigns(:topics)
56 56 end
57 57
58 58 def test_show_should_display_sticky_messages_first
59 59 Message.update_all(:sticky => 0)
60 60 Message.where({:id => 1}).update_all({:sticky => 1})
61 61
62 62 get :show, :project_id => 1, :id => 1
63 63 assert_response :success
64 64
65 65 topics = assigns(:topics)
66 66 assert_not_nil topics
67 67 assert topics.size > 1, "topics size was #{topics.size}"
68 68 assert topics.first.sticky?
69 69 assert topics.first.updated_on < topics.second.updated_on
70 70 end
71 71
72 72 def test_show_should_display_message_with_last_reply_first
73 73 Message.update_all(:sticky => 0)
74 74
75 75 # Reply to an old topic
76 76 old_topic = Message.where(:board_id => 1, :parent_id => nil).order('created_on ASC').first
77 77 reply = Message.new(:board_id => 1, :subject => 'New reply', :content => 'New reply', :author_id => 2)
78 78 old_topic.children << reply
79 79
80 80 get :show, :project_id => 1, :id => 1
81 81 assert_response :success
82 82 topics = assigns(:topics)
83 83 assert_not_nil topics
84 84 assert_equal old_topic, topics.first
85 85 end
86 86
87 87 def test_show_with_permission_should_display_the_new_message_form
88 88 @request.session[:user_id] = 2
89 89 get :show, :project_id => 1, :id => 1
90 90 assert_response :success
91 91 assert_template 'show'
92 92
93 93 assert_select 'form#message-form' do
94 94 assert_select 'input[name=?]', 'message[subject]'
95 95 end
96 96 end
97 97
98 98 def test_show_atom
99 99 get :show, :project_id => 1, :id => 1, :format => 'atom'
100 100 assert_response :success
101 101 assert_template 'common/feed'
102 102 assert_not_nil assigns(:board)
103 103 assert_not_nil assigns(:project)
104 104 assert_not_nil assigns(:messages)
105 105 end
106 106
107 107 def test_show_not_found
108 108 get :index, :project_id => 1, :id => 97
109 109 assert_response 404
110 110 end
111 111
112 112 def test_new
113 113 @request.session[:user_id] = 2
114 114 get :new, :project_id => 1
115 115 assert_response :success
116 116 assert_template 'new'
117 117
118 118 assert_select 'select[name=?]', 'board[parent_id]' do
119 119 assert_select 'option', (Project.find(1).boards.size + 1)
120 assert_select 'option[value=""]', :text => '&nbsp;'
120 assert_select 'option[value=""]'
121 121 assert_select 'option[value="1"]', :text => 'Help'
122 122 end
123
124 # &nbsp; replaced by nokogiri, not easy to test in DOM assertions
125 assert_not_include '<option value=""></option>', response.body
126 assert_include '<option value="">&nbsp;</option>', response.body
123 127 end
124 128
125 129 def test_new_without_project_boards
126 130 Project.find(1).boards.delete_all
127 131 @request.session[:user_id] = 2
128 132
129 133 get :new, :project_id => 1
130 134 assert_response :success
131 135 assert_template 'new'
132 136
133 137 assert_select 'select[name=?]', 'board[parent_id]', 0
134 138 end
135 139
136 140 def test_create
137 141 @request.session[:user_id] = 2
138 142 assert_difference 'Board.count' do
139 143 post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'}
140 144 end
141 145 assert_redirected_to '/projects/ecookbook/settings/boards'
142 146 board = Board.order('id DESC').first
143 147 assert_equal 'Testing', board.name
144 148 assert_equal 'Testing board creation', board.description
145 149 end
146 150
147 151 def test_create_with_parent
148 152 @request.session[:user_id] = 2
149 153 assert_difference 'Board.count' do
150 154 post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2}
151 155 end
152 156 assert_redirected_to '/projects/ecookbook/settings/boards'
153 157 board = Board.order('id DESC').first
154 158 assert_equal Board.find(2), board.parent
155 159 end
156 160
157 161 def test_create_with_failure
158 162 @request.session[:user_id] = 2
159 163 assert_no_difference 'Board.count' do
160 164 post :create, :project_id => 1, :board => { :name => '', :description => 'Testing board creation'}
161 165 end
162 166 assert_response :success
163 167 assert_template 'new'
164 168 end
165 169
166 170 def test_edit
167 171 @request.session[:user_id] = 2
168 172 get :edit, :project_id => 1, :id => 2
169 173 assert_response :success
170 174 assert_template 'edit'
171 175 end
172 176
173 177 def test_edit_with_parent
174 178 board = Board.generate!(:project_id => 1, :parent_id => 2)
175 179 @request.session[:user_id] = 2
176 180 get :edit, :project_id => 1, :id => board.id
177 181 assert_response :success
178 182 assert_template 'edit'
179 183
180 184 assert_select 'select[name=?]', 'board[parent_id]' do
181 185 assert_select 'option[value="2"][selected=selected]'
182 186 end
183 187 end
184 188
185 189 def test_update
186 190 @request.session[:user_id] = 2
187 191 assert_no_difference 'Board.count' do
188 192 put :update, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'}
189 193 end
190 194 assert_redirected_to '/projects/ecookbook/settings/boards'
191 195 assert_equal 'Testing', Board.find(2).name
192 196 end
193 197
194 198 def test_update_position
195 199 @request.session[:user_id] = 2
196 200 put :update, :project_id => 1, :id => 2, :board => { :move_to => 'highest'}
197 201 assert_redirected_to '/projects/ecookbook/settings/boards'
198 202 board = Board.find(2)
199 203 assert_equal 1, board.position
200 204 end
201 205
202 206 def test_update_with_failure
203 207 @request.session[:user_id] = 2
204 208 put :update, :project_id => 1, :id => 2, :board => { :name => '', :description => 'Testing board update'}
205 209 assert_response :success
206 210 assert_template 'edit'
207 211 end
208 212
209 213 def test_destroy
210 214 @request.session[:user_id] = 2
211 215 assert_difference 'Board.count', -1 do
212 216 delete :destroy, :project_id => 1, :id => 2
213 217 end
214 218 assert_redirected_to '/projects/ecookbook/settings/boards'
215 219 assert_nil Board.find_by_id(2)
216 220 end
217 221 end
@@ -1,288 +1,288
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ContextMenusControllerTest < ActionController::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :enabled_modules,
28 28 :workflows,
29 29 :journals, :journal_details,
30 30 :versions,
31 31 :issues, :issue_statuses, :issue_categories,
32 32 :users,
33 33 :enumerations,
34 34 :time_entries
35 35
36 36 def test_context_menu_one_issue
37 37 @request.session[:user_id] = 2
38 38 get :issues, :ids => [1]
39 39 assert_response :success
40 40 assert_template 'context_menus/issues'
41 41
42 42 assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit'
43 43 assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy'
44 44 assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete'
45 45
46 46 # Statuses
47 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5', :text => 'Closed'
48 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8', :text => 'Immediate'
47 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', :text => 'Closed'
48 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', :text => 'Immediate'
49 49 # No inactive priorities
50 50 assert_select 'a', :text => /Inactive Priority/, :count => 0
51 51 # Versions
52 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3', :text => '2.0'
53 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0'
52 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', :text => '2.0'
53 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0'
54 54 # Assignees
55 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper'
55 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper'
56 56 end
57 57
58 58 def test_context_menu_one_issue_by_anonymous
59 59 with_settings :default_language => 'en' do
60 60 get :issues, :ids => [1]
61 61 assert_response :success
62 62 assert_template 'context_menus/issues'
63 63 assert_select 'a.icon-del.disabled[href="#"]', :text => 'Delete'
64 64 end
65 65 end
66 66
67 67 def test_context_menu_multiple_issues_of_same_project
68 68 @request.session[:user_id] = 2
69 69 get :issues, :ids => [1, 2]
70 70 assert_response :success
71 71 assert_template 'context_menus/issues'
72 72 assert_not_nil assigns(:issues)
73 73 assert_equal [1, 2], assigns(:issues).map(&:id).sort
74 74
75 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
75 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&')
76 76
77 77 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
78 assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&amp;#{ids}", :text => 'Copy'
78 assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&#{ids}", :text => 'Copy'
79 79 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
80 80
81 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
82 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
83 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper'
81 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed'
82 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate'
83 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper'
84 84 end
85 85
86 86 def test_context_menu_multiple_issues_of_different_projects
87 87 @request.session[:user_id] = 2
88 88 get :issues, :ids => [1, 2, 6]
89 89 assert_response :success
90 90 assert_template 'context_menus/issues'
91 91 assert_not_nil assigns(:issues)
92 92 assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
93 93
94 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
94 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&')
95 95
96 96 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
97 97 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
98 98
99 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
100 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
101 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2", :text => 'John Smith'
99 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed'
100 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate'
101 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", :text => 'John Smith'
102 102 end
103 103
104 104 def test_context_menu_should_include_list_custom_fields
105 105 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
106 106 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
107 107 @request.session[:user_id] = 2
108 108 get :issues, :ids => [1]
109 109
110 110 assert_select "li.cf_#{field.id}" do
111 assert_select 'a[href=#]', :text => 'List'
111 assert_select 'a[href="#"]', :text => 'List'
112 112 assert_select 'ul' do
113 113 assert_select 'a', 3
114 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo'
115 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
114 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo'
115 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
116 116 end
117 117 end
118 118 end
119 119
120 120 def test_context_menu_should_not_include_null_value_for_required_custom_fields
121 121 field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list',
122 122 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
123 123 @request.session[:user_id] = 2
124 124 get :issues, :ids => [1, 2]
125 125
126 126 assert_select "li.cf_#{field.id}" do
127 assert_select 'a[href=#]', :text => 'List'
127 assert_select 'a[href="#"]', :text => 'List'
128 128 assert_select 'ul' do
129 129 assert_select 'a', 2
130 130 assert_select 'a', :text => 'none', :count => 0
131 131 end
132 132 end
133 133 end
134 134
135 135 def test_context_menu_on_single_issue_should_select_current_custom_field_value
136 136 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
137 137 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
138 138 issue = Issue.find(1)
139 139 issue.custom_field_values = {field.id => 'Bar'}
140 140 issue.save!
141 141 @request.session[:user_id] = 2
142 142 get :issues, :ids => [1]
143 143
144 144 assert_select "li.cf_#{field.id}" do
145 assert_select 'a[href=#]', :text => 'List'
145 assert_select 'a[href="#"]', :text => 'List'
146 146 assert_select 'ul' do
147 147 assert_select 'a', 3
148 148 assert_select 'a.icon-checked', :text => 'Bar'
149 149 end
150 150 end
151 151 end
152 152
153 153 def test_context_menu_should_include_bool_custom_fields
154 154 field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool',
155 155 :is_for_all => true, :tracker_ids => [1, 2, 3])
156 156 @request.session[:user_id] = 2
157 157 get :issues, :ids => [1]
158 158
159 159 assert_select "li.cf_#{field.id}" do
160 assert_select 'a[href=#]', :text => 'Bool'
160 assert_select 'a[href="#"]', :text => 'Bool'
161 161 assert_select 'ul' do
162 162 assert_select 'a', 3
163 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No'
164 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes'
165 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
163 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No'
164 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes'
165 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
166 166 end
167 167 end
168 168 end
169 169
170 170 def test_context_menu_should_include_user_custom_fields
171 171 field = IssueCustomField.create!(:name => 'User', :field_format => 'user',
172 172 :is_for_all => true, :tracker_ids => [1, 2, 3])
173 173 @request.session[:user_id] = 2
174 174 get :issues, :ids => [1]
175 175
176 176 assert_select "li.cf_#{field.id}" do
177 assert_select 'a[href=#]', :text => 'User'
177 assert_select 'a[href="#"]', :text => 'User'
178 178 assert_select 'ul' do
179 179 assert_select 'a', Project.find(1).members.count + 1
180 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith'
181 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
180 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith'
181 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
182 182 end
183 183 end
184 184 end
185 185
186 186 def test_context_menu_should_include_version_custom_fields
187 187 field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3])
188 188 @request.session[:user_id] = 2
189 189 get :issues, :ids => [1]
190 190
191 191 assert_select "li.cf_#{field.id}" do
192 assert_select 'a[href=#]', :text => 'Version'
192 assert_select 'a[href="#"]', :text => 'Version'
193 193 assert_select 'ul' do
194 194 assert_select 'a', Project.find(1).shared_versions.count + 1
195 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0'
196 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
195 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0'
196 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
197 197 end
198 198 end
199 199 end
200 200
201 201 def test_context_menu_should_show_enabled_custom_fields_for_the_role_only
202 202 enabled_cf = IssueCustomField.generate!(:field_format => 'bool', :is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
203 203 disabled_cf = IssueCustomField.generate!(:field_format => 'bool', :is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
204 204 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
205 205
206 206 @request.session[:user_id] = 2
207 207 get :issues, :ids => [issue.id]
208 208
209 209 assert_select "li.cf_#{enabled_cf.id}"
210 210 assert_select "li.cf_#{disabled_cf.id}", 0
211 211 end
212 212
213 213 def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
214 214 @request.session[:user_id] = 2
215 215 get :issues, :ids => [1]
216 216 assert_response :success
217 217 assert_template 'context_menus/issues'
218 218
219 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=2', :text => / me /
219 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', :text => / me /
220 220 end
221 221
222 222 def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects
223 223 @request.session[:user_id] = 2
224 224 version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1)
225 225
226 226 get :issues, :ids => [1, 4]
227 227 assert_response :success
228 228 assert_template 'context_menus/issues'
229 229
230 230 assert_include version, assigns(:versions)
231 231 assert_select 'a', :text => 'eCookbook - Shared'
232 232 end
233 233
234 234 def test_context_menu_with_issue_that_is_not_visible_should_fail
235 235 get :issues, :ids => [1, 4] # issue 4 is not visible
236 236 assert_response 302
237 237 end
238 238
239 239 def test_should_respond_with_404_without_ids
240 240 get :issues
241 241 assert_response 404
242 242 end
243 243
244 244 def test_time_entries_context_menu
245 245 @request.session[:user_id] = 2
246 246 get :time_entries, :ids => [1, 2]
247 247 assert_response :success
248 248 assert_template 'context_menus/time_entries'
249 249
250 250 assert_select 'a:not(.disabled)', :text => 'Edit'
251 251 end
252 252
253 253 def test_context_menu_for_one_time_entry
254 254 @request.session[:user_id] = 2
255 255 get :time_entries, :ids => [1]
256 256 assert_response :success
257 257 assert_template 'context_menus/time_entries'
258 258
259 259 assert_select 'a:not(.disabled)', :text => 'Edit'
260 260 end
261 261
262 262 def test_time_entries_context_menu_should_include_custom_fields
263 263 field = TimeEntryCustomField.generate!(:name => "Field", :field_format => "list", :possible_values => ["foo", "bar"])
264 264
265 265 @request.session[:user_id] = 2
266 266 get :time_entries, :ids => [1, 2]
267 267 assert_response :success
268 268 assert_select "li.cf_#{field.id}" do
269 assert_select 'a[href=#]', :text => "Field"
269 assert_select 'a[href="#"]', :text => "Field"
270 270 assert_select 'ul' do
271 271 assert_select 'a', 3
272 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=foo", :text => 'foo'
273 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=bar", :text => 'bar'
274 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
272 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=foo", :text => 'foo'
273 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=bar", :text => 'bar'
274 assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
275 275 end
276 276 end
277 277 end
278 278
279 279 def test_time_entries_context_menu_without_edit_permission
280 280 @request.session[:user_id] = 2
281 281 Role.find_by_name('Manager').remove_permission! :edit_time_entries
282 282
283 283 get :time_entries, :ids => [1, 2]
284 284 assert_response :success
285 285 assert_template 'context_menus/time_entries'
286 286 assert_select 'a.disabled', :text => 'Edit'
287 287 end
288 288 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,261 +1,261
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTransactionTest < ActionController::TestCase
22 22 tests IssuesController
23 23 fixtures :projects,
24 24 :users,
25 25 :roles,
26 26 :members,
27 27 :member_roles,
28 28 :issues,
29 29 :issue_statuses,
30 30 :versions,
31 31 :trackers,
32 32 :projects_trackers,
33 33 :issue_categories,
34 34 :enabled_modules,
35 35 :enumerations,
36 36 :attachments,
37 37 :workflows,
38 38 :custom_fields,
39 39 :custom_values,
40 40 :custom_fields_projects,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details,
45 45 :queries
46 46
47 47 self.use_transactional_fixtures = false
48 48
49 49 def setup
50 50 User.current = nil
51 51 end
52 52
53 53 def test_update_stale_issue_should_not_update_the_issue
54 54 issue = Issue.find(2)
55 55 @request.session[:user_id] = 2
56 56
57 57 assert_no_difference 'Journal.count' do
58 58 assert_no_difference 'TimeEntry.count' do
59 59 put :update,
60 60 :id => issue.id,
61 61 :issue => {
62 62 :fixed_version_id => 4,
63 63 :notes => 'My notes',
64 64 :lock_version => (issue.lock_version - 1)
65 65 },
66 66 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
67 67 end
68 68 end
69 69
70 70 assert_response :success
71 71 assert_template 'edit'
72 72
73 73 assert_select 'div.conflict'
74 74 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite'
75 75 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes'
76 76 assert_select 'label' do
77 77 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel'
78 78 assert_select 'a[href="/issues/2"]'
79 79 end
80 80 end
81 81
82 82 def test_update_stale_issue_should_save_attachments
83 83 set_tmp_attachments_directory
84 84 issue = Issue.find(2)
85 85 @request.session[:user_id] = 2
86 86
87 87 assert_no_difference 'Journal.count' do
88 88 assert_no_difference 'TimeEntry.count' do
89 89 assert_difference 'Attachment.count' do
90 90 put :update,
91 91 :id => issue.id,
92 92 :issue => {
93 93 :fixed_version_id => 4,
94 94 :notes => 'My notes',
95 95 :lock_version => (issue.lock_version - 1)
96 96 },
97 97 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
98 98 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
99 99 end
100 100 end
101 101 end
102 102
103 103 assert_response :success
104 104 assert_template 'edit'
105 105 attachment = Attachment.order('id DESC').first
106 106 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
107 107 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
108 108 end
109 109
110 110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
111 111 issue = Issue.find(2)
112 112 @request.session[:user_id] = 2
113 113
114 114 put :update, :id => issue.id,
115 115 :issue => {
116 116 :fixed_version_id => 4,
117 117 :notes => '',
118 118 :lock_version => (issue.lock_version - 1)
119 119 }
120 120
121 121 assert_select 'div.conflict'
122 122 assert_select 'input[name=conflict_resolution][value=overwrite]'
123 123 assert_select 'input[name=conflict_resolution][value=add_notes]', 0
124 124 assert_select 'input[name=conflict_resolution][value=cancel]'
125 125 end
126 126
127 127 def test_update_stale_issue_should_show_conflicting_journals
128 128 @request.session[:user_id] = 2
129 129
130 130 put :update, :id => 1,
131 131 :issue => {
132 132 :fixed_version_id => 4,
133 133 :notes => '',
134 134 :lock_version => 2
135 135 },
136 136 :last_journal_id => 1
137 137
138 138 assert_not_nil assigns(:conflict_journals)
139 139 assert_equal 1, assigns(:conflict_journals).size
140 140 assert_equal 2, assigns(:conflict_journals).first.id
141 141
142 142 assert_select 'div.conflict', :text => /Some notes with Redmine links/
143 143 end
144 144
145 145 def test_update_stale_issue_without_previous_journal_should_show_all_journals
146 146 @request.session[:user_id] = 2
147 147
148 148 put :update, :id => 1,
149 149 :issue => {
150 150 :fixed_version_id => 4,
151 151 :notes => '',
152 152 :lock_version => 2
153 153 },
154 154 :last_journal_id => ''
155 155
156 156 assert_not_nil assigns(:conflict_journals)
157 157 assert_equal 2, assigns(:conflict_journals).size
158 158 assert_select 'div.conflict', :text => /Some notes with Redmine links/
159 159 assert_select 'div.conflict', :text => /Journal notes/
160 160 end
161 161
162 162 def test_update_stale_issue_should_show_private_journals_with_permission_only
163 163 journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
164 164
165 165 @request.session[:user_id] = 2
166 166 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
167 167 assert_include journal, assigns(:conflict_journals)
168 168
169 169 Role.find(1).remove_permission! :view_private_notes
170 170 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
171 171 assert_not_include journal, assigns(:conflict_journals)
172 172 end
173 173
174 174 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
175 175 @request.session[:user_id] = 2
176 176
177 177 assert_difference 'Journal.count' do
178 178 put :update, :id => 1,
179 179 :issue => {
180 180 :fixed_version_id => 4,
181 181 :notes => 'overwrite_conflict_resolution',
182 182 :lock_version => 2
183 183 },
184 184 :conflict_resolution => 'overwrite'
185 185 end
186 186
187 187 assert_response 302
188 188 issue = Issue.find(1)
189 189 assert_equal 4, issue.fixed_version_id
190 190 journal = Journal.order('id DESC').first
191 191 assert_equal 'overwrite_conflict_resolution', journal.notes
192 192 assert journal.details.any?
193 193 end
194 194
195 195 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
196 196 @request.session[:user_id] = 2
197 197
198 198 assert_difference 'Journal.count' do
199 199 put :update, :id => 1,
200 200 :issue => {
201 201 :fixed_version_id => 4,
202 202 :notes => 'add_notes_conflict_resolution',
203 203 :lock_version => 2
204 204 },
205 205 :conflict_resolution => 'add_notes'
206 206 end
207 207
208 208 assert_response 302
209 209 issue = Issue.find(1)
210 210 assert_nil issue.fixed_version_id
211 211 journal = Journal.order('id DESC').first
212 212 assert_equal 'add_notes_conflict_resolution', journal.notes
213 213 assert journal.details.empty?
214 214 end
215 215
216 216 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
217 217 @request.session[:user_id] = 2
218 218
219 219 assert_no_difference 'Journal.count' do
220 220 put :update, :id => 1,
221 221 :issue => {
222 222 :fixed_version_id => 4,
223 223 :notes => 'add_notes_conflict_resolution',
224 224 :lock_version => 2
225 225 },
226 226 :conflict_resolution => 'cancel'
227 227 end
228 228
229 229 assert_redirected_to '/issues/1'
230 230 issue = Issue.find(1)
231 231 assert_nil issue.fixed_version_id
232 232 end
233 233
234 234 def test_put_update_with_spent_time_and_failure_should_not_add_spent_time
235 235 @request.session[:user_id] = 2
236 236
237 237 assert_no_difference('TimeEntry.count') do
238 238 put :update,
239 239 :id => 1,
240 240 :issue => { :subject => '' },
241 241 :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id }
242 242 assert_response :success
243 243 end
244 244
245 245 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5'
246 246 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added'
247 247 assert_select 'select[name=?]', 'time_entry[activity_id]' do
248 assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id
248 assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id.to_s
249 249 end
250 250 end
251 251
252 252 def test_index_should_rescue_invalid_sql_query
253 253 IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT")
254 254
255 255 get :index
256 256 assert_response 500
257 257 assert_select 'p', :text => /An error occurred/
258 258 assert_nil session[:query]
259 259 assert_nil session[:issues_index_sort]
260 260 end
261 261 end
@@ -1,322 +1,322
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssuesCustomFieldsVisibilityTest < ActionController::TestCase
21 21 tests IssuesController
22 22 fixtures :projects,
23 23 :users, :email_addresses,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issue_statuses,
28 28 :trackers,
29 29 :projects_trackers,
30 30 :enabled_modules,
31 31 :enumerations,
32 32 :workflows
33 33
34 34 def setup
35 35 CustomField.delete_all
36 36 Issue.delete_all
37 37 field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all}
38 38 @fields = []
39 39 @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true)))
40 40 @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2])))
41 41 @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3])))
42 42 @issue = Issue.generate!(
43 43 :author_id => 1,
44 44 :project_id => 1,
45 45 :tracker_id => 1,
46 46 :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'}
47 47 )
48 48
49 49 @user_with_role_on_other_project = User.generate!
50 50 User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3))
51 51
52 52 @users_to_test = {
53 53 User.find(1) => [@field1, @field2, @field3],
54 54 User.find(3) => [@field1, @field2],
55 55 @user_with_role_on_other_project => [@field1], # should see field1 only on Project 1
56 56 User.generate! => [@field1],
57 57 User.anonymous => [@field1]
58 58 }
59 59
60 60 Member.where(:project_id => 1).each do |member|
61 61 member.destroy unless @users_to_test.keys.include?(member.principal)
62 62 end
63 63 end
64 64
65 65 def test_show_should_show_visible_custom_fields_only
66 66 @users_to_test.each do |user, fields|
67 67 @request.session[:user_id] = user.id
68 68 get :show, :id => @issue.id
69 69 @fields.each_with_index do |field, i|
70 70 if fields.include?(field)
71 71 assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}"
72 72 else
73 73 assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}"
74 74 end
75 75 end
76 76 end
77 77 end
78 78
79 79 def test_show_should_show_visible_custom_fields_only_in_api
80 80 @users_to_test.each do |user, fields|
81 81 with_settings :rest_api_enabled => '1' do
82 82 get :show, :id => @issue.id, :format => 'xml', :include => 'custom_fields', :key => user.api_key
83 83 end
84 84 @fields.each_with_index do |field, i|
85 85 if fields.include?(field)
86 assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} in API"
86 assert_select "custom_field[id=?] value", field.id.to_s, {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} in API"
87 87 else
88 assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 0}, "User #{user.id} was not able to view #{field.name} in API"
88 assert_select "custom_field[id=?] value", field.id.to_s, {:text => "Value#{i}", :count => 0}, "User #{user.id} was not able to view #{field.name} in API"
89 89 end
90 90 end
91 91 end
92 92 end
93 93
94 94 def test_show_should_show_visible_custom_fields_only_in_history
95 95 @issue.init_journal(User.find(1))
96 96 @issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'}
97 97 @issue.save!
98 98
99 99 @users_to_test.each do |user, fields|
100 100 @request.session[:user_id] = user.id
101 101 get :show, :id => @issue.id
102 102 @fields.each_with_index do |field, i|
103 103 if fields.include?(field)
104 104 assert_select 'ul.details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change"
105 105 else
106 106 assert_select 'ul.details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change"
107 107 end
108 108 end
109 109 end
110 110 end
111 111
112 112 def test_show_should_show_visible_custom_fields_only_in_history_api
113 113 @issue.init_journal(User.find(1))
114 114 @issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'}
115 115 @issue.save!
116 116
117 117 @users_to_test.each do |user, fields|
118 118 with_settings :rest_api_enabled => '1' do
119 119 get :show, :id => @issue.id, :format => 'xml', :include => 'journals', :key => user.api_key
120 120 end
121 121 @fields.each_with_index do |field, i|
122 122 if fields.include?(field)
123 123 assert_select 'details old_value', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change in API"
124 124 else
125 125 assert_select 'details old_value', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change in API"
126 126 end
127 127 end
128 128 end
129 129 end
130 130
131 131 def test_edit_should_show_visible_custom_fields_only
132 132 Role.anonymous.add_permission! :edit_issues
133 133
134 134 @users_to_test.each do |user, fields|
135 135 @request.session[:user_id] = user.id
136 136 get :edit, :id => @issue.id
137 137 @fields.each_with_index do |field, i|
138 138 if fields.include?(field)
139 139 assert_select 'input[value=?]', "Value#{i}", 1, "User #{user.id} was not able to edit #{field.name}"
140 140 else
141 141 assert_select 'input[value=?]', "Value#{i}", 0, "User #{user.id} was able to edit #{field.name}"
142 142 end
143 143 end
144 144 end
145 145 end
146 146
147 147 def test_update_should_update_visible_custom_fields_only
148 148 Role.anonymous.add_permission! :edit_issues
149 149
150 150 @users_to_test.each do |user, fields|
151 151 @request.session[:user_id] = user.id
152 152 put :update, :id => @issue.id,
153 153 :issue => {:custom_field_values => {
154 154 @field1.id.to_s => "User#{user.id}Value0",
155 155 @field2.id.to_s => "User#{user.id}Value1",
156 156 @field3.id.to_s => "User#{user.id}Value2",
157 157 }}
158 158 @issue.reload
159 159 @fields.each_with_index do |field, i|
160 160 if fields.include?(field)
161 161 assert_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was not able to update #{field.name}"
162 162 else
163 163 assert_not_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was able to update #{field.name}"
164 164 end
165 165 end
166 166 end
167 167 end
168 168
169 169 def test_index_should_show_visible_custom_fields_only
170 170 @users_to_test.each do |user, fields|
171 171 @request.session[:user_id] = user.id
172 172 get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"})
173 173 @fields.each_with_index do |field, i|
174 174 if fields.include?(field)
175 175 assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}"
176 176 else
177 177 assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}"
178 178 end
179 179 end
180 180 end
181 181 end
182 182
183 183 def test_index_as_csv_should_show_visible_custom_fields_only
184 184 @users_to_test.each do |user, fields|
185 185 @request.session[:user_id] = user.id
186 186 get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"}), :format => 'csv'
187 187 @fields.each_with_index do |field, i|
188 188 if fields.include?(field)
189 189 assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV"
190 190 else
191 191 assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV"
192 192 end
193 193 end
194 194 end
195 195 end
196 196
197 197 def test_index_with_partial_custom_field_visibility
198 198 Issue.delete_all
199 199 p1 = Project.generate!
200 200 p2 = Project.generate!
201 201 user = User.generate!
202 202 User.add_to_project(user, p1, Role.where(:id => [1, 3]).to_a)
203 203 User.add_to_project(user, p2, Role.where(:id => 3).to_a)
204 204 Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueA'})
205 205 Issue.generate!(:project => p2, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueB'})
206 206 Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueC'})
207 207
208 208 @request.session[:user_id] = user.id
209 209 get :index, :c => ["subject", "cf_#{@field2.id}"]
210 210 assert_select 'td', :text => 'ValueA'
211 211 assert_select 'td', :text => 'ValueB', :count => 0
212 212 assert_select 'td', :text => 'ValueC'
213 213
214 214 get :index, :sort => "cf_#{@field2.id}"
215 215 # ValueB is not visible to user and ignored while sorting
216 216 assert_equal %w(ValueB ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
217 217
218 218 get :index, :set_filter => '1', "cf_#{@field2.id}" => '*'
219 219 assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
220 220
221 221 CustomField.update_all(:field_format => 'list')
222 222 get :index, :group => "cf_#{@field2.id}"
223 223 assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)}
224 224 end
225 225
226 226 def test_create_should_send_notifications_according_custom_fields_visibility
227 227 # anonymous user is never notified
228 228 users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
229 229
230 230 ActionMailer::Base.deliveries.clear
231 231 @request.session[:user_id] = 1
232 232 with_settings :bcc_recipients => '1' do
233 233 assert_difference 'Issue.count' do
234 234 post :create,
235 235 :project_id => 1,
236 236 :issue => {
237 237 :tracker_id => 1,
238 238 :status_id => 1,
239 239 :subject => 'New issue',
240 240 :priority_id => 5,
241 241 :custom_field_values => {@field1.id.to_s => 'Value0', @field2.id.to_s => 'Value1', @field3.id.to_s => 'Value2'},
242 242 :watcher_user_ids => users_to_test.keys.map(&:id)
243 243 }
244 244 assert_response 302
245 245 end
246 246 end
247 247 assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size
248 248 # tests that each user receives 1 email with the custom fields he is allowed to see only
249 249 users_to_test.each do |user, fields|
250 250 mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
251 251 assert_equal 1, mails.size
252 252 mail = mails.first
253 253 @fields.each_with_index do |field, i|
254 254 if fields.include?(field)
255 255 assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification"
256 256 else
257 257 assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification"
258 258 end
259 259 end
260 260 end
261 261 end
262 262
263 263 def test_update_should_send_notifications_according_custom_fields_visibility
264 264 # anonymous user is never notified
265 265 users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
266 266
267 267 users_to_test.keys.each do |user|
268 268 Watcher.create!(:user => user, :watchable => @issue)
269 269 end
270 270 ActionMailer::Base.deliveries.clear
271 271 @request.session[:user_id] = 1
272 272 with_settings :bcc_recipients => '1' do
273 273 put :update,
274 274 :id => @issue.id,
275 275 :issue => {
276 276 :custom_field_values => {@field1.id.to_s => 'NewValue0', @field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'}
277 277 }
278 278 assert_response 302
279 279 end
280 280 assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size
281 281 # tests that each user receives 1 email with the custom fields he is allowed to see only
282 282 users_to_test.each do |user, fields|
283 283 mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
284 284 assert_equal 1, mails.size
285 285 mail = mails.first
286 286 @fields.each_with_index do |field, i|
287 287 if fields.include?(field)
288 288 assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification"
289 289 else
290 290 assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification"
291 291 end
292 292 end
293 293 end
294 294 end
295 295
296 296 def test_updating_hidden_custom_fields_only_should_not_notifiy_user
297 297 # anonymous user is never notified
298 298 users_to_test = @users_to_test.reject {|k,v| k.anonymous?}
299 299
300 300 users_to_test.keys.each do |user|
301 301 Watcher.create!(:user => user, :watchable => @issue)
302 302 end
303 303 ActionMailer::Base.deliveries.clear
304 304 @request.session[:user_id] = 1
305 305 with_settings :bcc_recipients => '1' do
306 306 put :update,
307 307 :id => @issue.id,
308 308 :issue => {
309 309 :custom_field_values => {@field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'}
310 310 }
311 311 assert_response 302
312 312 end
313 313 users_to_test.each do |user, fields|
314 314 mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail}
315 315 if (fields & [@field2, @field3]).any?
316 316 assert_equal 1, mails.size, "User #{user.id} was not notified"
317 317 else
318 318 assert_equal 0, mails.size, "User #{user.id} was notified"
319 319 end
320 320 end
321 321 end
322 322 end
@@ -1,274 +1,274
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class MyControllerTest < ActionController::TestCase
21 21 fixtures :users, :email_addresses, :user_preferences, :roles, :projects, :members, :member_roles,
22 22 :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources
23 23
24 24 def setup
25 25 @request.session[:user_id] = 2
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'page'
32 32 end
33 33
34 34 def test_page
35 35 get :page
36 36 assert_response :success
37 37 assert_template 'page'
38 38 end
39 39
40 40 def test_page_with_timelog_block
41 41 preferences = User.find(2).pref
42 42 preferences[:my_page_layout] = {'top' => ['timelog']}
43 43 preferences.save!
44 44 TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10)
45 45
46 46 get :page
47 47 assert_response :success
48 48 assert_select 'tr.time-entry' do
49 49 assert_select 'td.subject a[href="/issues/1"]'
50 50 assert_select 'td.hours', :text => '2.50'
51 51 end
52 52 end
53 53
54 54 def test_page_with_all_blocks
55 55 blocks = MyController::BLOCKS.keys
56 56 preferences = User.find(2).pref
57 57 preferences[:my_page_layout] = {'top' => blocks}
58 58 preferences.save!
59 59
60 60 get :page
61 61 assert_response :success
62 62 assert_select 'div.mypage-box', blocks.size
63 63 end
64 64
65 65 def test_my_account_should_show_editable_custom_fields
66 66 get :account
67 67 assert_response :success
68 68 assert_template 'account'
69 69 assert_equal User.find(2), assigns(:user)
70 70
71 71 assert_select 'input[name=?]', 'user[custom_field_values][4]'
72 72 end
73 73
74 74 def test_my_account_should_not_show_non_editable_custom_fields
75 75 UserCustomField.find(4).update_attribute :editable, false
76 76
77 77 get :account
78 78 assert_response :success
79 79 assert_template 'account'
80 80 assert_equal User.find(2), assigns(:user)
81 81
82 82 assert_select 'input[name=?]', 'user[custom_field_values][4]', 0
83 83 end
84 84
85 85 def test_my_account_should_show_language_select
86 86 get :account
87 87 assert_response :success
88 88 assert_select 'select[name=?]', 'user[language]'
89 89 end
90 90
91 91 def test_my_account_should_not_show_language_select_with_force_default_language_for_loggedin
92 92 with_settings :force_default_language_for_loggedin => '1' do
93 93 get :account
94 94 assert_response :success
95 95 assert_select 'select[name=?]', 'user[language]', 0
96 96 end
97 97 end
98 98
99 99 def test_update_account
100 100 post :account,
101 101 :user => {
102 102 :firstname => "Joe",
103 103 :login => "root",
104 104 :admin => 1,
105 105 :group_ids => ['10'],
106 106 :custom_field_values => {"4" => "0100562500"}
107 107 }
108 108
109 109 assert_redirected_to '/my/account'
110 110 user = User.find(2)
111 111 assert_equal user, assigns(:user)
112 112 assert_equal "Joe", user.firstname
113 113 assert_equal "jsmith", user.login
114 114 assert_equal "0100562500", user.custom_value_for(4).value
115 115 # ignored
116 116 assert !user.admin?
117 117 assert user.groups.empty?
118 118 end
119 119
120 120 def test_my_account_should_show_destroy_link
121 121 get :account
122 122 assert_select 'a[href="/my/account/destroy"]'
123 123 end
124 124
125 125 def test_get_destroy_should_display_the_destroy_confirmation
126 126 get :destroy
127 127 assert_response :success
128 128 assert_template 'destroy'
129 assert_select 'form[action=/my/account/destroy]' do
129 assert_select 'form[action="/my/account/destroy"]' do
130 130 assert_select 'input[name=confirm]'
131 131 end
132 132 end
133 133
134 134 def test_post_destroy_without_confirmation_should_not_destroy_account
135 135 assert_no_difference 'User.count' do
136 136 post :destroy
137 137 end
138 138 assert_response :success
139 139 assert_template 'destroy'
140 140 end
141 141
142 142 def test_post_destroy_without_confirmation_should_destroy_account
143 143 assert_difference 'User.count', -1 do
144 144 post :destroy, :confirm => '1'
145 145 end
146 146 assert_redirected_to '/'
147 147 assert_match /deleted/i, flash[:notice]
148 148 end
149 149
150 150 def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account
151 151 User.any_instance.stubs(:own_account_deletable?).returns(false)
152 152
153 153 assert_no_difference 'User.count' do
154 154 post :destroy, :confirm => '1'
155 155 end
156 156 assert_redirected_to '/my/account'
157 157 end
158 158
159 159 def test_change_password
160 160 get :password
161 161 assert_response :success
162 162 assert_template 'password'
163 163
164 164 # non matching password confirmation
165 165 post :password, :password => 'jsmith',
166 166 :new_password => 'secret123',
167 167 :new_password_confirmation => 'secret1234'
168 168 assert_response :success
169 169 assert_template 'password'
170 170 assert_select_error /Password doesn.*t match confirmation/
171 171
172 172 # wrong password
173 173 post :password, :password => 'wrongpassword',
174 174 :new_password => 'secret123',
175 175 :new_password_confirmation => 'secret123'
176 176 assert_response :success
177 177 assert_template 'password'
178 178 assert_equal 'Wrong password', flash[:error]
179 179
180 180 # good password
181 181 post :password, :password => 'jsmith',
182 182 :new_password => 'secret123',
183 183 :new_password_confirmation => 'secret123'
184 184 assert_redirected_to '/my/account'
185 185 assert User.try_to_login('jsmith', 'secret123')
186 186 end
187 187
188 188 def test_change_password_kills_other_sessions
189 189 @request.session[:ctime] = (Time.now - 30.minutes).utc.to_i
190 190
191 191 jsmith = User.find(2)
192 192 jsmith.passwd_changed_on = Time.now
193 193 jsmith.save!
194 194
195 195 get 'account'
196 196 assert_response 302
197 197 assert flash[:error].match(/Your session has expired/)
198 198 end
199 199
200 200 def test_change_password_should_redirect_if_user_cannot_change_its_password
201 201 User.find(2).update_attribute(:auth_source_id, 1)
202 202
203 203 get :password
204 204 assert_not_nil flash[:error]
205 205 assert_redirected_to '/my/account'
206 206 end
207 207
208 208 def test_page_layout
209 209 get :page_layout
210 210 assert_response :success
211 211 assert_template 'page_layout'
212 212 end
213 213
214 214 def test_add_block
215 215 post :add_block, :block => 'issuesreportedbyme'
216 216 assert_redirected_to '/my/page_layout'
217 217 assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme')
218 218 end
219 219
220 220 def test_add_invalid_block_should_redirect
221 221 post :add_block, :block => 'invalid'
222 222 assert_redirected_to '/my/page_layout'
223 223 end
224 224
225 225 def test_remove_block
226 226 post :remove_block, :block => 'issuesassignedtome'
227 227 assert_redirected_to '/my/page_layout'
228 228 assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome')
229 229 end
230 230
231 231 def test_order_blocks
232 232 xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews']
233 233 assert_response :success
234 234 assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left']
235 235 end
236 236
237 237 def test_reset_rss_key_with_existing_key
238 238 @previous_token_value = User.find(2).rss_key # Will generate one if it's missing
239 239 post :reset_rss_key
240 240
241 241 assert_not_equal @previous_token_value, User.find(2).rss_key
242 242 assert User.find(2).rss_token
243 243 assert_match /reset/, flash[:notice]
244 244 assert_redirected_to '/my/account'
245 245 end
246 246
247 247 def test_reset_rss_key_without_existing_key
248 248 assert_nil User.find(2).rss_token
249 249 post :reset_rss_key
250 250
251 251 assert User.find(2).rss_token
252 252 assert_match /reset/, flash[:notice]
253 253 assert_redirected_to '/my/account'
254 254 end
255 255
256 256 def test_reset_api_key_with_existing_key
257 257 @previous_token_value = User.find(2).api_key # Will generate one if it's missing
258 258 post :reset_api_key
259 259
260 260 assert_not_equal @previous_token_value, User.find(2).api_key
261 261 assert User.find(2).api_token
262 262 assert_match /reset/, flash[:notice]
263 263 assert_redirected_to '/my/account'
264 264 end
265 265
266 266 def test_reset_api_key_without_existing_key
267 267 assert_nil User.find(2).api_token
268 268 post :reset_api_key
269 269
270 270 assert User.find(2).api_token
271 271 assert_match /reset/, flash[:notice]
272 272 assert_redirected_to '/my/account'
273 273 end
274 274 end
@@ -1,233 +1,233
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoriesBazaarControllerTest < ActionController::TestCase
21 21 tests RepositoriesController
22 22
23 23 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
24 24 :repositories, :enabled_modules
25 25
26 26 REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s
27 27 REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk")
28 28 PRJ_ID = 3
29 29 CHAR_1_UTF8_HEX = "\xc3\x9c".dup.force_encoding('UTF-8')
30 30
31 31 def setup
32 32 User.current = nil
33 33 @project = Project.find(PRJ_ID)
34 34 @repository = Repository::Bazaar.create(
35 35 :project => @project,
36 36 :url => REPOSITORY_PATH_TRUNK,
37 37 :log_encoding => 'UTF-8')
38 38 assert @repository
39 39 end
40 40
41 41 if File.directory?(REPOSITORY_PATH)
42 42 def test_get_new
43 43 @request.session[:user_id] = 1
44 44 @project.repository.destroy
45 45 get :new, :project_id => 'subproject1', :repository_scm => 'Bazaar'
46 46 assert_response :success
47 47 assert_template 'new'
48 48 assert_kind_of Repository::Bazaar, assigns(:repository)
49 49 assert assigns(:repository).new_record?
50 50 end
51 51
52 52 def test_browse_root
53 53 get :show, :id => PRJ_ID
54 54 assert_response :success
55 55 assert_template 'show'
56 56 assert_not_nil assigns(:entries)
57 57 assert_equal 2, assigns(:entries).size
58 58 assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'}
59 59 assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'}
60 60 end
61 61
62 62 def test_browse_directory
63 63 get :show, :id => PRJ_ID, :path => repository_path_hash(['directory'])[:param]
64 64 assert_response :success
65 65 assert_template 'show'
66 66 assert_not_nil assigns(:entries)
67 67 assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name)
68 68 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
69 69 assert_not_nil entry
70 70 assert_equal 'file', entry.kind
71 71 assert_equal 'directory/edit.png', entry.path
72 72 end
73 73
74 74 def test_browse_at_given_revision
75 75 get :show, :id => PRJ_ID, :path => repository_path_hash([])[:param],
76 76 :rev => 3
77 77 assert_response :success
78 78 assert_template 'show'
79 79 assert_not_nil assigns(:entries)
80 80 assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'],
81 81 assigns(:entries).collect(&:name)
82 82 end
83 83
84 84 def test_changes
85 85 get :changes, :id => PRJ_ID,
86 86 :path => repository_path_hash(['doc-mkdir.txt'])[:param]
87 87 assert_response :success
88 88 assert_template 'changes'
89 89 assert_select 'h2', :text => /doc-mkdir.txt/
90 90 end
91 91
92 92 def test_entry_show
93 93 get :entry, :id => PRJ_ID,
94 94 :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param]
95 95 assert_response :success
96 96 assert_template 'entry'
97 97 # Line 19
98 98 assert_select 'tr#L29 td.line-code', :text => /Show help message/
99 99 end
100 100
101 101 def test_entry_download
102 102 get :entry, :id => PRJ_ID,
103 103 :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param],
104 104 :format => 'raw'
105 105 assert_response :success
106 106 # File content
107 107 assert @response.body.include?('Show help message')
108 108 end
109 109
110 110 def test_directory_entry
111 111 get :entry, :id => PRJ_ID,
112 112 :path => repository_path_hash(['directory'])[:param]
113 113 assert_response :success
114 114 assert_template 'show'
115 115 assert_not_nil assigns(:entry)
116 116 assert_equal 'directory', assigns(:entry).name
117 117 end
118 118
119 119 def test_diff
120 120 # Full diff of changeset 3
121 121 ['inline', 'sbs'].each do |dt|
122 122 get :diff, :id => PRJ_ID, :rev => 3, :type => dt
123 123 assert_response :success
124 124 assert_template 'diff'
125 125 # Line 11 removed
126 assert_select 'th.line-num:content(11) ~ td.diff_out', :text => /Display more information/
126 assert_select 'th.line-num:contains(11) ~ td.diff_out', :text => /Display more information/
127 127 end
128 128 end
129 129
130 130 def test_annotate
131 131 get :annotate, :id => PRJ_ID,
132 132 :path => repository_path_hash(['doc-mkdir.txt'])[:param]
133 133 assert_response :success
134 134 assert_template 'annotate'
135 135 assert_select "th.line-num", :text => '2' do
136 136 assert_select "+ td.revision" do
137 137 assert_select "a", :text => '3'
138 138 assert_select "+ td.author", :text => "jsmith@" do
139 139 assert_select "+ td",
140 140 :text => "Main purpose:"
141 141 end
142 142 end
143 143 end
144 144 end
145 145
146 146 def test_annotate_author_escaping
147 147 repository = Repository::Bazaar.create(
148 148 :project => @project,
149 149 :url => File.join(REPOSITORY_PATH, "author_escaping"),
150 150 :identifier => 'author_escaping',
151 151 :log_encoding => 'UTF-8')
152 152 assert repository
153 153 get :annotate, :id => PRJ_ID, :repository_id => 'author_escaping',
154 154 :path => repository_path_hash(['author-escaping-test.txt'])[:param]
155 155 assert_response :success
156 156 assert_template 'annotate'
157 157 assert_select "th.line-num", :text => '1' do
158 158 assert_select "+ td.revision" do
159 159 assert_select "a", :text => '2'
160 assert_select "+ td.author", :text => "test &amp;" do
160 assert_select "+ td.author", :text => "test &" do
161 161 assert_select "+ td",
162 162 :text => "author escaping test"
163 163 end
164 164 end
165 165 end
166 166 end
167 167
168 168 def test_annotate_author_non_ascii
169 169 log_encoding = nil
170 170 if Encoding.locale_charmap == "UTF-8" ||
171 171 Encoding.locale_charmap == "ISO-8859-1"
172 172 log_encoding = Encoding.locale_charmap
173 173 end
174 174 unless log_encoding.nil?
175 175 repository = Repository::Bazaar.create(
176 176 :project => @project,
177 177 :url => File.join(REPOSITORY_PATH, "author_non_ascii"),
178 178 :identifier => 'author_non_ascii',
179 179 :log_encoding => log_encoding)
180 180 assert repository
181 181 get :annotate, :id => PRJ_ID, :repository_id => 'author_non_ascii',
182 182 :path => repository_path_hash(['author-non-ascii-test.txt'])[:param]
183 183 assert_response :success
184 184 assert_template 'annotate'
185 185 assert_select "th.line-num", :text => '1' do
186 186 assert_select "+ td.revision" do
187 187 assert_select "a", :text => '2'
188 188 assert_select "+ td.author", :text => "test #{CHAR_1_UTF8_HEX}" do
189 189 assert_select "+ td",
190 190 :text => "author non ASCII test"
191 191 end
192 192 end
193 193 end
194 194 end
195 195 end
196 196
197 197 def test_destroy_valid_repository
198 198 @request.session[:user_id] = 1 # admin
199 199 assert_equal 0, @repository.changesets.count
200 200 @repository.fetch_changesets
201 201 assert @repository.changesets.count > 0
202 202
203 203 assert_difference 'Repository.count', -1 do
204 204 delete :destroy, :id => @repository.id
205 205 end
206 206 assert_response 302
207 207 @project.reload
208 208 assert_nil @project.repository
209 209 end
210 210
211 211 def test_destroy_invalid_repository
212 212 @request.session[:user_id] = 1 # admin
213 213 @project.repository.destroy
214 214 @repository = Repository::Bazaar.create!(
215 215 :project => @project,
216 216 :url => "/invalid",
217 217 :log_encoding => 'UTF-8')
218 218 @repository.fetch_changesets
219 219 @repository.reload
220 220 assert_equal 0, @repository.changesets.count
221 221
222 222 assert_difference 'Repository.count', -1 do
223 223 delete :destroy, :id => @repository.id
224 224 end
225 225 assert_response 302
226 226 @project.reload
227 227 assert_nil @project.repository
228 228 end
229 229 else
230 230 puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!"
231 231 def test_fake; assert true end
232 232 end
233 233 end
@@ -1,304 +1,298
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoriesControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, :enabled_modules,
22 22 :repositories, :issues, :issue_statuses, :changesets, :changes,
23 23 :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
24 24
25 25 def setup
26 26 User.current = nil
27 27 end
28 28
29 29 def test_new
30 30 @request.session[:user_id] = 1
31 31 get :new, :project_id => 'subproject1'
32 32 assert_response :success
33 33 assert_template 'new'
34 34 assert_kind_of Repository::Subversion, assigns(:repository)
35 35 assert assigns(:repository).new_record?
36 36 assert_select 'input[name=?]:not([disabled])', 'repository[url]'
37 37 end
38 38
39 39 def test_new_should_propose_enabled_scm_only
40 40 @request.session[:user_id] = 1
41 41 with_settings :enabled_scm => ['Mercurial', 'Git'] do
42 42 get :new, :project_id => 'subproject1'
43 43 end
44 44 assert_response :success
45 45 assert_template 'new'
46 46 assert_kind_of Repository::Mercurial, assigns(:repository)
47 47
48 48 assert_select 'select[name=repository_scm]' do
49 49 assert_select 'option', 3
50 50 assert_select 'option[value=Mercurial][selected=selected]'
51 51 assert_select 'option[value=Git]:not([selected])'
52 52 end
53 53 end
54 54
55 55 def test_create
56 56 @request.session[:user_id] = 1
57 57 assert_difference 'Repository.count' do
58 58 post :create, :project_id => 'subproject1',
59 59 :repository_scm => 'Subversion',
60 60 :repository => {:url => 'file:///test', :is_default => '1', :identifier => ''}
61 61 end
62 62 assert_response 302
63 63 repository = Repository.order('id DESC').first
64 64 assert_kind_of Repository::Subversion, repository
65 65 assert_equal 'file:///test', repository.url
66 66 end
67 67
68 68 def test_create_with_failure
69 69 @request.session[:user_id] = 1
70 70 assert_no_difference 'Repository.count' do
71 71 post :create, :project_id => 'subproject1',
72 72 :repository_scm => 'Subversion',
73 73 :repository => {:url => 'invalid'}
74 74 end
75 75 assert_response :success
76 76 assert_template 'new'
77 77 assert_kind_of Repository::Subversion, assigns(:repository)
78 78 assert assigns(:repository).new_record?
79 79 end
80 80
81 81 def test_edit
82 82 @request.session[:user_id] = 1
83 83 get :edit, :id => 11
84 84 assert_response :success
85 85 assert_template 'edit'
86 86 assert_equal Repository.find(11), assigns(:repository)
87 87 assert_select 'input[name=?][value=?][disabled=disabled]', 'repository[url]', 'svn://localhost/test'
88 88 end
89 89
90 90 def test_update
91 91 @request.session[:user_id] = 1
92 92 put :update, :id => 11, :repository => {:password => 'test_update'}
93 93 assert_response 302
94 94 assert_equal 'test_update', Repository.find(11).password
95 95 end
96 96
97 97 def test_update_with_failure
98 98 @request.session[:user_id] = 1
99 99 put :update, :id => 11, :repository => {:password => 'x'*260}
100 100 assert_response :success
101 101 assert_template 'edit'
102 102 assert_equal Repository.find(11), assigns(:repository)
103 103 end
104 104
105 105 def test_destroy
106 106 @request.session[:user_id] = 1
107 107 assert_difference 'Repository.count', -1 do
108 108 delete :destroy, :id => 11
109 109 end
110 110 assert_response 302
111 111 assert_nil Repository.find_by_id(11)
112 112 end
113 113
114 114 def test_show_with_autofetch_changesets_enabled_should_fetch_changesets
115 115 Repository::Subversion.any_instance.expects(:fetch_changesets).once
116 116
117 117 with_settings :autofetch_changesets => '1' do
118 118 get :show, :id => 1
119 119 end
120 120 end
121 121
122 122 def test_show_with_autofetch_changesets_disabled_should_not_fetch_changesets
123 123 Repository::Subversion.any_instance.expects(:fetch_changesets).never
124 124
125 125 with_settings :autofetch_changesets => '0' do
126 126 get :show, :id => 1
127 127 end
128 128 end
129 129
130 130 def test_show_with_closed_project_should_not_fetch_changesets
131 131 Repository::Subversion.any_instance.expects(:fetch_changesets).never
132 132 Project.find(1).close
133 133
134 134 with_settings :autofetch_changesets => '1' do
135 135 get :show, :id => 1
136 136 end
137 137 end
138 138
139 139 def test_revisions
140 140 get :revisions, :id => 1
141 141 assert_response :success
142 142 assert_template 'revisions'
143 143 assert_equal Repository.find(10), assigns(:repository)
144 144 assert_not_nil assigns(:changesets)
145 145 end
146 146
147 147 def test_revisions_for_other_repository
148 148 repository = Repository::Subversion.create!(:project_id => 1, :identifier => 'foo', :url => 'file:///foo')
149 149
150 150 get :revisions, :id => 1, :repository_id => 'foo'
151 151 assert_response :success
152 152 assert_template 'revisions'
153 153 assert_equal repository, assigns(:repository)
154 154 assert_not_nil assigns(:changesets)
155 155 end
156 156
157 157 def test_revisions_for_invalid_repository
158 158 get :revisions, :id => 1, :repository_id => 'foo'
159 159 assert_response 404
160 160 end
161 161
162 162 def test_revision
163 163 get :revision, :id => 1, :rev => 1
164 164 assert_response :success
165 165 assert_not_nil assigns(:changeset)
166 166 assert_equal "1", assigns(:changeset).revision
167 167 end
168 168
169 169 def test_revision_should_show_add_related_issue_form
170 170 Role.find(1).add_permission! :manage_related_issues
171 171 @request.session[:user_id] = 2
172 172
173 173 get :revision, :id => 1, :rev => 1
174 174 assert_response :success
175 175
176 176 assert_select 'form[action=?]', '/projects/ecookbook/repository/revisions/1/issues' do
177 177 assert_select 'input[name=?]', 'issue_id'
178 178 end
179 179 end
180 180
181 181 def test_revision_should_not_change_the_project_menu_link
182 182 get :revision, :id => 1, :rev => 1
183 183 assert_response :success
184 184
185 185 assert_select '#main-menu a.repository[href=?]', '/projects/ecookbook/repository'
186 186 end
187 187
188 188 def test_revision_with_before_nil_and_afer_normal
189 189 get :revision, {:id => 1, :rev => 1}
190 190 assert_response :success
191 191 assert_template 'revision'
192 192
193 193 assert_select 'div.contextual' do
194 194 assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/0', 0
195 195 assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2'
196 196 end
197 197 end
198 198
199 199 def test_add_related_issue
200 200 @request.session[:user_id] = 2
201 201 assert_difference 'Changeset.find(103).issues.size' do
202 202 xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js'
203 203 assert_response :success
204 204 assert_template 'add_related_issue'
205 205 assert_equal 'text/javascript', response.content_type
206 206 end
207 207 assert_equal [2], Changeset.find(103).issue_ids
208 208 assert_include 'related-issues', response.body
209 209 assert_include 'Feature request #2', response.body
210 210 end
211 211
212 212 def test_add_related_issue_should_accept_issue_id_with_sharp
213 213 @request.session[:user_id] = 2
214 214 assert_difference 'Changeset.find(103).issues.size' do
215 215 xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => "#2", :format => 'js'
216 216 end
217 217 assert_equal [2], Changeset.find(103).issue_ids
218 218 end
219 219
220 220 def test_add_related_issue_with_invalid_issue_id
221 221 @request.session[:user_id] = 2
222 222 assert_no_difference 'Changeset.find(103).issues.size' do
223 223 xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 9999, :format => 'js'
224 224 assert_response :success
225 225 assert_template 'add_related_issue'
226 226 assert_equal 'text/javascript', response.content_type
227 227 end
228 228 assert_include 'alert("Issue is invalid")', response.body
229 229 end
230 230
231 231 def test_remove_related_issue
232 232 Changeset.find(103).issues << Issue.find(1)
233 233 Changeset.find(103).issues << Issue.find(2)
234 234
235 235 @request.session[:user_id] = 2
236 236 assert_difference 'Changeset.find(103).issues.size', -1 do
237 237 xhr :delete, :remove_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js'
238 238 assert_response :success
239 239 assert_template 'remove_related_issue'
240 240 assert_equal 'text/javascript', response.content_type
241 241 end
242 242 assert_equal [1], Changeset.find(103).issue_ids
243 243 assert_include 'related-issue-2', response.body
244 244 end
245 245
246 246 def test_graph_commits_per_month
247 247 # Make sure there's some data to display
248 248 latest = Project.find(1).repository.changesets.maximum(:commit_date)
249 249 assert_not_nil latest
250 250 Date.stubs(:today).returns(latest.to_date + 10)
251 251
252 252 get :graph, :id => 1, :graph => 'commits_per_month'
253 253 assert_response :success
254 254 assert_equal 'image/svg+xml', @response.content_type
255 255 end
256 256
257 257 def test_graph_commits_per_author
258 258 get :graph, :id => 1, :graph => 'commits_per_author'
259 259 assert_response :success
260 260 assert_equal 'image/svg+xml', @response.content_type
261 261 end
262 262
263 263 def test_get_committers
264 264 @request.session[:user_id] = 2
265 265 # add a commit with an unknown user
266 266 Changeset.create!(
267 267 :repository => Project.find(1).repository,
268 268 :committer => 'foo',
269 269 :committed_on => Time.now,
270 270 :revision => 100,
271 271 :comments => 'Committed by foo.'
272 272 )
273 273
274 274 get :committers, :id => 10
275 275 assert_response :success
276 276 assert_template 'committers'
277 277
278 assert_select 'td:content(dlopper) + td select' do
279 assert_select 'option[value="3"][selected=selected]', :text => 'Dave Lopper'
280 end
281
282 assert_select 'td:content(foo) + td select' do
283 assert_select 'option[value=""]'
284 assert_select 'option[selected=selected]', 0 # no option selected
285 end
278 assert_select 'input[value=dlopper] + select option[value="3"][selected=selected]', :text => 'Dave Lopper'
279 assert_select 'input[value=foo] + select option[selected=selected]', 0 # no option selected
286 280 end
287 281
288 282 def test_post_committers
289 283 @request.session[:user_id] = 2
290 284 # add a commit with an unknown user
291 285 c = Changeset.create!(
292 286 :repository => Project.find(1).repository,
293 287 :committer => 'foo',
294 288 :committed_on => Time.now,
295 289 :revision => 100,
296 290 :comments => 'Committed by foo.'
297 291 )
298 292 assert_no_difference "Changeset.where(:user_id => 3).count" do
299 293 post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
300 294 assert_response 302
301 295 assert_equal User.find(2), c.reload.user
302 296 end
303 297 end
304 298 end
@@ -1,161 +1,161
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoriesDarcsControllerTest < ActionController::TestCase
21 21 tests RepositoriesController
22 22
23 23 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
24 24 :repositories, :enabled_modules
25 25
26 26 REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s
27 27 PRJ_ID = 3
28 28 NUM_REV = 6
29 29
30 30 def setup
31 31 User.current = nil
32 32 @project = Project.find(PRJ_ID)
33 33 @repository = Repository::Darcs.create(
34 34 :project => @project,
35 35 :url => REPOSITORY_PATH,
36 36 :log_encoding => 'UTF-8'
37 37 )
38 38 assert @repository
39 39 end
40 40
41 41 if File.directory?(REPOSITORY_PATH)
42 42 def test_get_new
43 43 @request.session[:user_id] = 1
44 44 @project.repository.destroy
45 45 get :new, :project_id => 'subproject1', :repository_scm => 'Darcs'
46 46 assert_response :success
47 47 assert_template 'new'
48 48 assert_kind_of Repository::Darcs, assigns(:repository)
49 49 assert assigns(:repository).new_record?
50 50 end
51 51
52 52 def test_browse_root
53 53 assert_equal 0, @repository.changesets.count
54 54 @repository.fetch_changesets
55 55 @project.reload
56 56 assert_equal NUM_REV, @repository.changesets.count
57 57 get :show, :id => PRJ_ID
58 58 assert_response :success
59 59 assert_template 'show'
60 60 assert_not_nil assigns(:entries)
61 61 assert_equal 3, assigns(:entries).size
62 62 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
63 63 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
64 64 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
65 65 end
66 66
67 67 def test_browse_directory
68 68 assert_equal 0, @repository.changesets.count
69 69 @repository.fetch_changesets
70 70 @project.reload
71 71 assert_equal NUM_REV, @repository.changesets.count
72 72 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
73 73 assert_response :success
74 74 assert_template 'show'
75 75 assert_not_nil assigns(:entries)
76 76 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
77 77 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
78 78 assert_not_nil entry
79 79 assert_equal 'file', entry.kind
80 80 assert_equal 'images/edit.png', entry.path
81 81 end
82 82
83 83 def test_browse_at_given_revision
84 84 assert_equal 0, @repository.changesets.count
85 85 @repository.fetch_changesets
86 86 @project.reload
87 87 assert_equal NUM_REV, @repository.changesets.count
88 88 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
89 89 :rev => 1
90 90 assert_response :success
91 91 assert_template 'show'
92 92 assert_not_nil assigns(:entries)
93 93 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
94 94 end
95 95
96 96 def test_changes
97 97 assert_equal 0, @repository.changesets.count
98 98 @repository.fetch_changesets
99 99 @project.reload
100 100 assert_equal NUM_REV, @repository.changesets.count
101 101 get :changes, :id => PRJ_ID,
102 102 :path => repository_path_hash(['images', 'edit.png'])[:param]
103 103 assert_response :success
104 104 assert_template 'changes'
105 105 assert_select 'h2', :text => /edit.png/
106 106 end
107 107
108 108 def test_diff
109 109 assert_equal 0, @repository.changesets.count
110 110 @repository.fetch_changesets
111 111 @project.reload
112 112 assert_equal NUM_REV, @repository.changesets.count
113 113 # Full diff of changeset 5
114 114 ['inline', 'sbs'].each do |dt|
115 115 get :diff, :id => PRJ_ID, :rev => 5, :type => dt
116 116 assert_response :success
117 117 assert_template 'diff'
118 118 # Line 22 removed
119 assert_select 'th.line-num:content(22) ~ td.diff_out', :text => /def remove/
119 assert_select 'th.line-num:contains(22) ~ td.diff_out', :text => /def remove/
120 120 end
121 121 end
122 122
123 123 def test_destroy_valid_repository
124 124 @request.session[:user_id] = 1 # admin
125 125 assert_equal 0, @repository.changesets.count
126 126 @repository.fetch_changesets
127 127 @project.reload
128 128 assert_equal NUM_REV, @repository.changesets.count
129 129
130 130 assert_difference 'Repository.count', -1 do
131 131 delete :destroy, :id => @repository.id
132 132 end
133 133 assert_response 302
134 134 @project.reload
135 135 assert_nil @project.repository
136 136 end
137 137
138 138 def test_destroy_invalid_repository
139 139 @request.session[:user_id] = 1 # admin
140 140 @project.repository.destroy
141 141 @repository = Repository::Darcs.create!(
142 142 :project => @project,
143 143 :url => "/invalid",
144 144 :log_encoding => 'UTF-8'
145 145 )
146 146 @repository.fetch_changesets
147 147 @project.reload
148 148 assert_equal 0, @repository.changesets.count
149 149
150 150 assert_difference 'Repository.count', -1 do
151 151 delete :destroy, :id => @repository.id
152 152 end
153 153 assert_response 302
154 154 @project.reload
155 155 assert_nil @project.repository
156 156 end
157 157 else
158 158 puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
159 159 def test_fake; assert true end
160 160 end
161 161 end
@@ -1,606 +1,606
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoriesGitControllerTest < ActionController::TestCase
21 21 tests RepositoriesController
22 22
23 23 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
24 24 :repositories, :enabled_modules
25 25
26 26 REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s
27 27 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
28 28 PRJ_ID = 3
29 29 CHAR_1_HEX = "\xc3\x9c".force_encoding('UTF-8')
30 30 FELIX_HEX = "Felix Sch\xC3\xA4fer".force_encoding('UTF-8')
31 31 NUM_REV = 28
32 32
33 33 ## Git, Mercurial and CVS path encodings are binary.
34 34 ## Subversion supports URL encoding for path.
35 35 ## Redmine Mercurial adapter and extension use URL encoding.
36 36 ## Git accepts only binary path in command line parameter.
37 37 ## So, there is no way to use binary command line parameter in JRuby.
38 38 JRUBY_SKIP = (RUBY_PLATFORM == 'java')
39 39 JRUBY_SKIP_STR = "TODO: This test fails in JRuby"
40 40
41 41 def setup
42 42 @ruby19_non_utf8_pass = Encoding.default_external.to_s != 'UTF-8'
43 43
44 44 User.current = nil
45 45 @project = Project.find(PRJ_ID)
46 46 @repository = Repository::Git.create(
47 47 :project => @project,
48 48 :url => REPOSITORY_PATH,
49 49 :path_encoding => 'ISO-8859-1'
50 50 )
51 51 assert @repository
52 52 end
53 53
54 54 def test_create_and_update
55 55 @request.session[:user_id] = 1
56 56 assert_difference 'Repository.count' do
57 57 post :create, :project_id => 'subproject1',
58 58 :repository_scm => 'Git',
59 59 :repository => {
60 60 :url => '/test',
61 61 :is_default => '0',
62 62 :identifier => 'test-create',
63 63 :extra_report_last_commit => '1',
64 64 }
65 65 end
66 66 assert_response 302
67 67 repository = Repository.order('id DESC').first
68 68 assert_kind_of Repository::Git, repository
69 69 assert_equal '/test', repository.url
70 70 assert_equal true, repository.extra_report_last_commit
71 71
72 72 put :update, :id => repository.id,
73 73 :repository => {
74 74 :extra_report_last_commit => '0'
75 75 }
76 76 assert_response 302
77 77 repo2 = Repository.find(repository.id)
78 78 assert_equal false, repo2.extra_report_last_commit
79 79 end
80 80
81 81 if File.directory?(REPOSITORY_PATH)
82 82 ## Ruby uses ANSI api to fork a process on Windows.
83 83 ## Japanese Shift_JIS and Traditional Chinese Big5 have 0x5c(backslash) problem
84 84 ## and these are incompatible with ASCII.
85 85 ## Git for Windows (msysGit) changed internal API from ANSI to Unicode in 1.7.10
86 86 ## http://code.google.com/p/msysgit/issues/detail?id=80
87 87 ## So, Latin-1 path tests fail on Japanese Windows
88 88 WINDOWS_PASS = (Redmine::Platform.mswin? &&
89 89 Redmine::Scm::Adapters::GitAdapter.client_version_above?([1, 7, 10]))
90 90 WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10"
91 91
92 92 def test_get_new
93 93 @request.session[:user_id] = 1
94 94 @project.repository.destroy
95 95 get :new, :project_id => 'subproject1', :repository_scm => 'Git'
96 96 assert_response :success
97 97 assert_template 'new'
98 98 assert_kind_of Repository::Git, assigns(:repository)
99 99 assert assigns(:repository).new_record?
100 100 end
101 101
102 102 def test_browse_root
103 103 assert_equal 0, @repository.changesets.count
104 104 @repository.fetch_changesets
105 105 @project.reload
106 106 assert_equal NUM_REV, @repository.changesets.count
107 107
108 108 get :show, :id => PRJ_ID
109 109 assert_response :success
110 110 assert_template 'show'
111 111 assert_not_nil assigns(:entries)
112 112 assert_equal 9, assigns(:entries).size
113 113 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
114 114 assert assigns(:entries).detect {|e| e.name == 'this_is_a_really_long_and_verbose_directory_name' && e.kind == 'dir'}
115 115 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
116 116 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
117 117 assert assigns(:entries).detect {|e| e.name == 'copied_README' && e.kind == 'file'}
118 118 assert assigns(:entries).detect {|e| e.name == 'new_file.txt' && e.kind == 'file'}
119 119 assert assigns(:entries).detect {|e| e.name == 'renamed_test.txt' && e.kind == 'file'}
120 120 assert assigns(:entries).detect {|e| e.name == 'filemane with spaces.txt' && e.kind == 'file'}
121 121 assert assigns(:entries).detect {|e| e.name == ' filename with a leading space.txt ' && e.kind == 'file'}
122 122 assert_not_nil assigns(:changesets)
123 123 assert assigns(:changesets).size > 0
124 124 end
125 125
126 126 def test_browse_branch
127 127 assert_equal 0, @repository.changesets.count
128 128 @repository.fetch_changesets
129 129 @project.reload
130 130 assert_equal NUM_REV, @repository.changesets.count
131 131 get :show, :id => PRJ_ID, :rev => 'test_branch'
132 132 assert_response :success
133 133 assert_template 'show'
134 134 assert_not_nil assigns(:entries)
135 135 assert_equal 4, assigns(:entries).size
136 136 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
137 137 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
138 138 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
139 139 assert assigns(:entries).detect {|e| e.name == 'test.txt' && e.kind == 'file'}
140 140 assert_not_nil assigns(:changesets)
141 141 assert assigns(:changesets).size > 0
142 142 end
143 143
144 144 def test_browse_tag
145 145 assert_equal 0, @repository.changesets.count
146 146 @repository.fetch_changesets
147 147 @project.reload
148 148 assert_equal NUM_REV, @repository.changesets.count
149 149 [
150 150 "tag00.lightweight",
151 151 "tag01.annotated",
152 152 ].each do |t1|
153 153 get :show, :id => PRJ_ID, :rev => t1
154 154 assert_response :success
155 155 assert_template 'show'
156 156 assert_not_nil assigns(:entries)
157 157 assert assigns(:entries).size > 0
158 158 assert_not_nil assigns(:changesets)
159 159 assert assigns(:changesets).size > 0
160 160 end
161 161 end
162 162
163 163 def test_browse_directory
164 164 assert_equal 0, @repository.changesets.count
165 165 @repository.fetch_changesets
166 166 @project.reload
167 167 assert_equal NUM_REV, @repository.changesets.count
168 168 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
169 169 assert_response :success
170 170 assert_template 'show'
171 171 assert_not_nil assigns(:entries)
172 172 assert_equal ['edit.png'], assigns(:entries).collect(&:name)
173 173 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
174 174 assert_not_nil entry
175 175 assert_equal 'file', entry.kind
176 176 assert_equal 'images/edit.png', entry.path
177 177 assert_not_nil assigns(:changesets)
178 178 assert assigns(:changesets).size > 0
179 179 end
180 180
181 181 def test_browse_at_given_revision
182 182 assert_equal 0, @repository.changesets.count
183 183 @repository.fetch_changesets
184 184 @project.reload
185 185 assert_equal NUM_REV, @repository.changesets.count
186 186 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
187 187 :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518'
188 188 assert_response :success
189 189 assert_template 'show'
190 190 assert_not_nil assigns(:entries)
191 191 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
192 192 assert_not_nil assigns(:changesets)
193 193 assert assigns(:changesets).size > 0
194 194 end
195 195
196 196 def test_changes
197 197 get :changes, :id => PRJ_ID,
198 198 :path => repository_path_hash(['images', 'edit.png'])[:param]
199 199 assert_response :success
200 200 assert_template 'changes'
201 201 assert_select 'h2', :text => /edit.png/
202 202 end
203 203
204 204 def test_entry_show
205 205 get :entry, :id => PRJ_ID,
206 206 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
207 207 assert_response :success
208 208 assert_template 'entry'
209 209 # Line 11
210 210 assert_select 'tr#L11 td.line-code', :text => /WITHOUT ANY WARRANTY/
211 211 end
212 212
213 213 def test_entry_show_latin_1
214 214 if @ruby19_non_utf8_pass
215 215 puts_ruby19_non_utf8_pass()
216 216 elsif WINDOWS_PASS
217 217 puts WINDOWS_SKIP_STR
218 218 elsif JRUBY_SKIP
219 219 puts JRUBY_SKIP_STR
220 220 else
221 221 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
222 222 ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
223 223 get :entry, :id => PRJ_ID,
224 224 :path => repository_path_hash(['latin-1-dir', "test-#{CHAR_1_HEX}.txt"])[:param],
225 225 :rev => r1
226 226 assert_response :success
227 227 assert_template 'entry'
228 228 assert_select 'tr#L1 td.line-code', :text => /test-#{CHAR_1_HEX}.txt/
229 229 end
230 230 end
231 231 end
232 232 end
233 233
234 234 def test_entry_download
235 235 get :entry, :id => PRJ_ID,
236 236 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
237 237 :format => 'raw'
238 238 assert_response :success
239 239 # File content
240 240 assert @response.body.include?('WITHOUT ANY WARRANTY')
241 241 end
242 242
243 243 def test_directory_entry
244 244 get :entry, :id => PRJ_ID,
245 245 :path => repository_path_hash(['sources'])[:param]
246 246 assert_response :success
247 247 assert_template 'show'
248 248 assert_not_nil assigns(:entry)
249 249 assert_equal 'sources', assigns(:entry).name
250 250 end
251 251
252 252 def test_diff
253 253 assert_equal true, @repository.is_default
254 254 assert_nil @repository.identifier
255 255 assert_equal 0, @repository.changesets.count
256 256 @repository.fetch_changesets
257 257 @project.reload
258 258 assert_equal NUM_REV, @repository.changesets.count
259 259 # Full diff of changeset 2f9c0091
260 260 ['inline', 'sbs'].each do |dt|
261 261 get :diff,
262 262 :id => PRJ_ID,
263 263 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
264 264 :type => dt
265 265 assert_response :success
266 266 assert_template 'diff'
267 267 # Line 22 removed
268 assert_select 'th.line-num:content(22) ~ td.diff_out', :text => /def remove/
268 assert_select 'th.line-num:contains(22) ~ td.diff_out', :text => /def remove/
269 269 assert_select 'h2', :text => /2f9c0091/
270 270 end
271 271 end
272 272
273 273 def test_diff_with_rev_and_path
274 274 assert_equal 0, @repository.changesets.count
275 275 @repository.fetch_changesets
276 276 @project.reload
277 277 assert_equal NUM_REV, @repository.changesets.count
278 278 with_settings :diff_max_lines_displayed => 1000 do
279 279 # Full diff of changeset 2f9c0091
280 280 ['inline', 'sbs'].each do |dt|
281 281 get :diff,
282 282 :id => PRJ_ID,
283 283 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
284 284 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
285 285 :type => dt
286 286 assert_response :success
287 287 assert_template 'diff'
288 288 # Line 22 removed
289 assert_select 'th.line-num:content(22) ~ td.diff_out', :text => /def remove/
289 assert_select 'th.line-num:contains(22) ~ td.diff_out', :text => /def remove/
290 290 assert_select 'h2', :text => /2f9c0091/
291 291 end
292 292 end
293 293 end
294 294
295 295 def test_diff_truncated
296 296 assert_equal 0, @repository.changesets.count
297 297 @repository.fetch_changesets
298 298 @project.reload
299 299 assert_equal NUM_REV, @repository.changesets.count
300 300
301 301 with_settings :diff_max_lines_displayed => 5 do
302 302 # Truncated diff of changeset 2f9c0091
303 303 with_cache do
304 304 with_settings :default_language => 'en' do
305 305 get :diff, :id => PRJ_ID, :type => 'inline',
306 306 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
307 307 assert_response :success
308 308 assert @response.body.include?("... This diff was truncated")
309 309 end
310 310 with_settings :default_language => 'fr' do
311 311 get :diff, :id => PRJ_ID, :type => 'inline',
312 312 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
313 313 assert_response :success
314 314 assert ! @response.body.include?("... This diff was truncated")
315 315 assert @response.body.include?("... Ce diff")
316 316 end
317 317 end
318 318 end
319 319 end
320 320
321 321 def test_diff_two_revs
322 322 assert_equal 0, @repository.changesets.count
323 323 @repository.fetch_changesets
324 324 @project.reload
325 325 assert_equal NUM_REV, @repository.changesets.count
326 326 ['inline', 'sbs'].each do |dt|
327 327 get :diff,
328 328 :id => PRJ_ID,
329 329 :rev => '61b685fbe55ab05b5ac68402d5720c1a6ac973d1',
330 330 :rev_to => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
331 331 :type => dt
332 332 assert_response :success
333 333 assert_template 'diff'
334 334 diff = assigns(:diff)
335 335 assert_not_nil diff
336 336 assert_select 'h2', :text => /2f9c0091:61b685fb/
337 337 assert_select 'form[action=?]', '/projects/subproject1/repository/revisions/61b685fbe55ab05b5ac68402d5720c1a6ac973d1/diff'
338 338 assert_select 'input#rev_to[type=hidden][name=rev_to][value=?]', '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
339 339 end
340 340 end
341 341
342 342 def test_diff_path_in_subrepo
343 343 repo = Repository::Git.create(
344 344 :project => @project,
345 345 :url => REPOSITORY_PATH,
346 346 :identifier => 'test-diff-path',
347 347 :path_encoding => 'ISO-8859-1'
348 348 )
349 349 assert repo
350 350 assert_equal false, repo.is_default
351 351 assert_equal 'test-diff-path', repo.identifier
352 352 get :diff,
353 353 :id => PRJ_ID,
354 354 :repository_id => 'test-diff-path',
355 355 :rev => '61b685fbe55ab05b',
356 356 :rev_to => '2f9c0091c754a91a',
357 357 :type => 'inline'
358 358 assert_response :success
359 359 assert_template 'diff'
360 360 diff = assigns(:diff)
361 361 assert_not_nil diff
362 362 assert_select 'form[action=?]', '/projects/subproject1/repository/test-diff-path/revisions/61b685fbe55ab05b/diff'
363 363 assert_select 'input#rev_to[type=hidden][name=rev_to][value=?]', '2f9c0091c754a91a'
364 364 end
365 365
366 366 def test_diff_latin_1
367 367 if @ruby19_non_utf8_pass
368 368 puts_ruby19_non_utf8_pass()
369 369 else
370 370 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
371 371 ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
372 372 ['inline', 'sbs'].each do |dt|
373 373 get :diff, :id => PRJ_ID, :rev => r1, :type => dt
374 374 assert_response :success
375 375 assert_template 'diff'
376 376 assert_select 'table' do
377 377 assert_select 'thead th.filename', :text => /latin-1-dir\/test-#{CHAR_1_HEX}.txt/
378 378 assert_select 'tbody td.diff_in', :text => /test-#{CHAR_1_HEX}.txt/
379 379 end
380 380 end
381 381 end
382 382 end
383 383 end
384 384 end
385 385
386 386 def test_diff_should_show_filenames
387 387 get :diff, :id => PRJ_ID, :rev => 'deff712f05a90d96edbd70facc47d944be5897e3', :type => 'inline'
388 388 assert_response :success
389 389 assert_template 'diff'
390 390 # modified file
391 391 assert_select 'th.filename', :text => 'sources/watchers_controller.rb'
392 392 # deleted file
393 393 assert_select 'th.filename', :text => 'test.txt'
394 394 end
395 395
396 396 def test_save_diff_type
397 397 user1 = User.find(1)
398 398 user1.pref[:diff_type] = nil
399 399 user1.preference.save
400 400 user = User.find(1)
401 401 assert_nil user.pref[:diff_type]
402 402
403 403 @request.session[:user_id] = 1 # admin
404 404 get :diff,
405 405 :id => PRJ_ID,
406 406 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
407 407 assert_response :success
408 408 assert_template 'diff'
409 409 user.reload
410 410 assert_equal "inline", user.pref[:diff_type]
411 411 get :diff,
412 412 :id => PRJ_ID,
413 413 :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
414 414 :type => 'sbs'
415 415 assert_response :success
416 416 assert_template 'diff'
417 417 user.reload
418 418 assert_equal "sbs", user.pref[:diff_type]
419 419 end
420 420
421 421 def test_annotate
422 422 get :annotate, :id => PRJ_ID,
423 423 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
424 424 assert_response :success
425 425 assert_template 'annotate'
426 426
427 427 # Line 23, changeset 2f9c0091
428 428 assert_select 'tr' do
429 429 assert_select 'th.line-num', :text => '23'
430 430 assert_select 'td.revision', :text => /2f9c0091/
431 431 assert_select 'td.author', :text => 'jsmith'
432 432 assert_select 'td', :text => /remove_watcher/
433 433 end
434 434 end
435 435
436 436 def test_annotate_at_given_revision
437 437 assert_equal 0, @repository.changesets.count
438 438 @repository.fetch_changesets
439 439 @project.reload
440 440 assert_equal NUM_REV, @repository.changesets.count
441 441 get :annotate, :id => PRJ_ID, :rev => 'deff7',
442 442 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
443 443 assert_response :success
444 444 assert_template 'annotate'
445 445 assert_select 'h2', :text => /@ deff712f/
446 446 end
447 447
448 448 def test_annotate_binary_file
449 449 with_settings :default_language => 'en' do
450 450 get :annotate, :id => PRJ_ID,
451 451 :path => repository_path_hash(['images', 'edit.png'])[:param]
452 452 assert_response 500
453 453 assert_select 'p#errorExplanation', :text => /cannot be annotated/
454 454 end
455 455 end
456 456
457 457 def test_annotate_error_when_too_big
458 458 with_settings :file_max_size_displayed => 1 do
459 459 get :annotate, :id => PRJ_ID,
460 460 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
461 461 :rev => 'deff712f'
462 462 assert_response 500
463 463 assert_select 'p#errorExplanation', :text => /exceeds the maximum text file size/
464 464
465 465 get :annotate, :id => PRJ_ID,
466 466 :path => repository_path_hash(['README'])[:param],
467 467 :rev => '7234cb2'
468 468 assert_response :success
469 469 assert_template 'annotate'
470 470 end
471 471 end
472 472
473 473 def test_annotate_latin_1
474 474 if @ruby19_non_utf8_pass
475 475 puts_ruby19_non_utf8_pass()
476 476 elsif WINDOWS_PASS
477 477 puts WINDOWS_SKIP_STR
478 478 elsif JRUBY_SKIP
479 479 puts JRUBY_SKIP_STR
480 480 else
481 481 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
482 482 ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
483 483 get :annotate, :id => PRJ_ID,
484 484 :path => repository_path_hash(['latin-1-dir', "test-#{CHAR_1_HEX}.txt"])[:param],
485 485 :rev => r1
486 486 assert_select "th.line-num", :text => '1' do
487 487 assert_select "+ td.revision" do
488 488 assert_select "a", :text => '57ca437c'
489 489 assert_select "+ td.author", :text => "jsmith" do
490 490 assert_select "+ td",
491 491 :text => "test-#{CHAR_1_HEX}.txt"
492 492 end
493 493 end
494 494 end
495 495 end
496 496 end
497 497 end
498 498 end
499 499
500 500 def test_annotate_latin_1_author
501 501 ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', '83ca5fd546063a'].each do |r1|
502 502 get :annotate, :id => PRJ_ID,
503 503 :path => repository_path_hash([" filename with a leading space.txt "])[:param],
504 504 :rev => r1
505 505 assert_select "th.line-num", :text => '1' do
506 506 assert_select "+ td.revision" do
507 507 assert_select "a", :text => '83ca5fd5'
508 508 assert_select "+ td.author", :text => FELIX_HEX do
509 509 assert_select "+ td",
510 510 :text => "And this is a file with a leading and trailing space..."
511 511 end
512 512 end
513 513 end
514 514 end
515 515 end
516 516
517 517 def test_revisions
518 518 assert_equal 0, @repository.changesets.count
519 519 @repository.fetch_changesets
520 520 @project.reload
521 521 assert_equal NUM_REV, @repository.changesets.count
522 522 get :revisions, :id => PRJ_ID
523 523 assert_response :success
524 524 assert_template 'revisions'
525 525 assert_select 'form[method=get][action=?]', '/projects/subproject1/repository/revision'
526 526 end
527 527
528 528 def test_revision
529 529 assert_equal 0, @repository.changesets.count
530 530 @repository.fetch_changesets
531 531 @project.reload
532 532 assert_equal NUM_REV, @repository.changesets.count
533 533 ['61b685fbe55ab05b5ac68402d5720c1a6ac973d1', '61b685f'].each do |r|
534 534 get :revision, :id => PRJ_ID, :rev => r
535 535 assert_response :success
536 536 assert_template 'revision'
537 537 end
538 538 end
539 539
540 540 def test_empty_revision
541 541 assert_equal 0, @repository.changesets.count
542 542 @repository.fetch_changesets
543 543 @project.reload
544 544 assert_equal NUM_REV, @repository.changesets.count
545 545 ['', ' ', nil].each do |r|
546 546 get :revision, :id => PRJ_ID, :rev => r
547 547 assert_response 404
548 548 assert_select_error /was not found/
549 549 end
550 550 end
551 551
552 552 def test_destroy_valid_repository
553 553 @request.session[:user_id] = 1 # admin
554 554 assert_equal 0, @repository.changesets.count
555 555 @repository.fetch_changesets
556 556 @project.reload
557 557 assert_equal NUM_REV, @repository.changesets.count
558 558
559 559 assert_difference 'Repository.count', -1 do
560 560 delete :destroy, :id => @repository.id
561 561 end
562 562 assert_response 302
563 563 @project.reload
564 564 assert_nil @project.repository
565 565 end
566 566
567 567 def test_destroy_invalid_repository
568 568 @request.session[:user_id] = 1 # admin
569 569 @project.repository.destroy
570 570 @repository = Repository::Git.create!(
571 571 :project => @project,
572 572 :url => "/invalid",
573 573 :path_encoding => 'ISO-8859-1'
574 574 )
575 575 @repository.fetch_changesets
576 576 @repository.reload
577 577 assert_equal 0, @repository.changesets.count
578 578
579 579 assert_difference 'Repository.count', -1 do
580 580 delete :destroy, :id => @repository.id
581 581 end
582 582 assert_response 302
583 583 @project.reload
584 584 assert_nil @project.repository
585 585 end
586 586
587 587 private
588 588
589 589 def puts_ruby19_non_utf8_pass
590 590 puts "TODO: This test fails " +
591 591 "when Encoding.default_external is not UTF-8. " +
592 592 "Current value is '#{Encoding.default_external.to_s}'"
593 593 end
594 594 else
595 595 puts "Git test repository NOT FOUND. Skipping functional tests !!!"
596 596 def test_fake; assert true end
597 597 end
598 598
599 599 private
600 600 def with_cache(&block)
601 601 before = ActionController::Base.perform_caching
602 602 ActionController::Base.perform_caching = true
603 603 block.call
604 604 ActionController::Base.perform_caching = before
605 605 end
606 606 end
@@ -1,490 +1,490
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoriesMercurialControllerTest < ActionController::TestCase
21 21 tests RepositoriesController
22 22
23 23 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
24 24 :repositories, :enabled_modules
25 25
26 26 REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s
27 27 CHAR_1_HEX = "\xc3\x9c"
28 28 PRJ_ID = 3
29 29 NUM_REV = 34
30 30
31 31 ruby19_non_utf8_pass = Encoding.default_external.to_s != 'UTF-8'
32 32
33 33 def setup
34 34 User.current = nil
35 35 @project = Project.find(PRJ_ID)
36 36 @repository = Repository::Mercurial.create(
37 37 :project => @project,
38 38 :url => REPOSITORY_PATH,
39 39 :path_encoding => 'ISO-8859-1'
40 40 )
41 41 assert @repository
42 42 @diff_c_support = true
43 43 @char_1 = CHAR_1_HEX.dup.force_encoding('UTF-8')
44 44 @tag_char_1 = "tag-#{CHAR_1_HEX}-00".force_encoding('UTF-8')
45 45 @branch_char_0 = "branch-#{CHAR_1_HEX}-00".force_encoding('UTF-8')
46 46 @branch_char_1 = "branch-#{CHAR_1_HEX}-01".force_encoding('UTF-8')
47 47 end
48 48
49 49 if ruby19_non_utf8_pass
50 50 puts "TODO: Mercurial functional test fails " +
51 51 "when Encoding.default_external is not UTF-8. " +
52 52 "Current value is '#{Encoding.default_external.to_s}'"
53 53 def test_fake; assert true end
54 54 elsif File.directory?(REPOSITORY_PATH)
55 55
56 56 def test_get_new
57 57 @request.session[:user_id] = 1
58 58 @project.repository.destroy
59 59 get :new, :project_id => 'subproject1', :repository_scm => 'Mercurial'
60 60 assert_response :success
61 61 assert_template 'new'
62 62 assert_kind_of Repository::Mercurial, assigns(:repository)
63 63 assert assigns(:repository).new_record?
64 64 end
65 65
66 66 def test_show_root
67 67 assert_equal 0, @repository.changesets.count
68 68 @repository.fetch_changesets
69 69 @project.reload
70 70 assert_equal NUM_REV, @repository.changesets.count
71 71 get :show, :id => PRJ_ID
72 72 assert_response :success
73 73 assert_template 'show'
74 74 assert_not_nil assigns(:entries)
75 75 assert_equal 4, assigns(:entries).size
76 76 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
77 77 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
78 78 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
79 79 assert_not_nil assigns(:changesets)
80 80 assert assigns(:changesets).size > 0
81 81 end
82 82
83 83 def test_show_directory
84 84 assert_equal 0, @repository.changesets.count
85 85 @repository.fetch_changesets
86 86 @project.reload
87 87 assert_equal NUM_REV, @repository.changesets.count
88 88 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
89 89 assert_response :success
90 90 assert_template 'show'
91 91 assert_not_nil assigns(:entries)
92 92 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
93 93 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
94 94 assert_not_nil entry
95 95 assert_equal 'file', entry.kind
96 96 assert_equal 'images/edit.png', entry.path
97 97 assert_not_nil assigns(:changesets)
98 98 assert assigns(:changesets).size > 0
99 99 end
100 100
101 101 def test_show_at_given_revision
102 102 assert_equal 0, @repository.changesets.count
103 103 @repository.fetch_changesets
104 104 @project.reload
105 105 assert_equal NUM_REV, @repository.changesets.count
106 106 [0, '0', '0885933ad4f6'].each do |r1|
107 107 get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
108 108 :rev => r1
109 109 assert_response :success
110 110 assert_template 'show'
111 111 assert_not_nil assigns(:entries)
112 112 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
113 113 assert_not_nil assigns(:changesets)
114 114 assert assigns(:changesets).size > 0
115 115 end
116 116 end
117 117
118 118 def test_show_directory_sql_escape_percent
119 119 assert_equal 0, @repository.changesets.count
120 120 @repository.fetch_changesets
121 121 @project.reload
122 122 assert_equal NUM_REV, @repository.changesets.count
123 123 [13, '13', '3a330eb32958'].each do |r1|
124 124 get :show, :id => PRJ_ID,
125 125 :path => repository_path_hash(['sql_escape', 'percent%dir'])[:param],
126 126 :rev => r1
127 127 assert_response :success
128 128 assert_template 'show'
129 129
130 130 assert_not_nil assigns(:entries)
131 131 assert_equal ['percent%file1.txt', 'percentfile1.txt'],
132 132 assigns(:entries).collect(&:name)
133 133 changesets = assigns(:changesets)
134 134 assert_not_nil changesets
135 135 assert assigns(:changesets).size > 0
136 136 assert_equal %w(13 11 10 9), changesets.collect(&:revision)
137 137 end
138 138 end
139 139
140 140 def test_show_directory_latin_1_path
141 141 assert_equal 0, @repository.changesets.count
142 142 @repository.fetch_changesets
143 143 @project.reload
144 144 assert_equal NUM_REV, @repository.changesets.count
145 145 [21, '21', 'adf805632193'].each do |r1|
146 146 get :show, :id => PRJ_ID,
147 147 :path => repository_path_hash(['latin-1-dir'])[:param],
148 148 :rev => r1
149 149 assert_response :success
150 150 assert_template 'show'
151 151
152 152 assert_not_nil assigns(:entries)
153 153 assert_equal ["make-latin-1-file.rb",
154 154 "test-#{@char_1}-1.txt",
155 155 "test-#{@char_1}-2.txt",
156 156 "test-#{@char_1}.txt"], assigns(:entries).collect(&:name)
157 157 changesets = assigns(:changesets)
158 158 assert_not_nil changesets
159 159 assert_equal %w(21 20 19 18 17), changesets.collect(&:revision)
160 160 end
161 161 end
162 162
163 163 def show_should_show_branch_selection_form
164 164 @repository.fetch_changesets
165 165 @project.reload
166 166 get :show, :id => PRJ_ID
167 167 assert_select 'form#revision_selector[action=?]', '/projects/subproject1/repository/show' do
168 168 assert_select 'select[name=branch]' do
169 169 assert_select 'option[value=?]', 'test-branch-01'
170 170 end
171 171 end
172 172 end
173 173
174 174 def test_show_branch
175 175 assert_equal 0, @repository.changesets.count
176 176 @repository.fetch_changesets
177 177 @project.reload
178 178 assert_equal NUM_REV, @repository.changesets.count
179 179 [
180 180 'default',
181 181 @branch_char_1,
182 182 'branch (1)[2]&,%.-3_4',
183 183 @branch_char_0,
184 184 'test_branch.latin-1',
185 185 'test-branch-00',
186 186 ].each do |bra|
187 187 get :show, :id => PRJ_ID, :rev => bra
188 188 assert_response :success
189 189 assert_template 'show'
190 190 assert_not_nil assigns(:entries)
191 191 assert assigns(:entries).size > 0
192 192 assert_not_nil assigns(:changesets)
193 193 assert assigns(:changesets).size > 0
194 194 end
195 195 end
196 196
197 197 def test_show_tag
198 198 assert_equal 0, @repository.changesets.count
199 199 @repository.fetch_changesets
200 200 @project.reload
201 201 assert_equal NUM_REV, @repository.changesets.count
202 202 [
203 203 @tag_char_1,
204 204 'tag_test.00',
205 205 'tag-init-revision'
206 206 ].each do |tag|
207 207 get :show, :id => PRJ_ID, :rev => tag
208 208 assert_response :success
209 209 assert_template 'show'
210 210 assert_not_nil assigns(:entries)
211 211 assert assigns(:entries).size > 0
212 212 assert_not_nil assigns(:changesets)
213 213 assert assigns(:changesets).size > 0
214 214 end
215 215 end
216 216
217 217 def test_changes
218 218 get :changes, :id => PRJ_ID,
219 219 :path => repository_path_hash(['images', 'edit.png'])[:param]
220 220 assert_response :success
221 221 assert_template 'changes'
222 222 assert_select 'h2', :text => /edit.png/
223 223 end
224 224
225 225 def test_entry_show
226 226 get :entry, :id => PRJ_ID,
227 227 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
228 228 assert_response :success
229 229 assert_template 'entry'
230 230 # Line 10
231 231 assert_select 'tr#L10 td.line-code', :text => /WITHOUT ANY WARRANTY/
232 232 end
233 233
234 234 def test_entry_show_latin_1_path
235 235 [21, '21', 'adf805632193'].each do |r1|
236 236 get :entry, :id => PRJ_ID,
237 237 :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param],
238 238 :rev => r1
239 239 assert_response :success
240 240 assert_template 'entry'
241 241 assert_select 'tr#L1 td.line-code', :text => /Mercurial is a distributed version control system/
242 242 end
243 243 end
244 244
245 245 def test_entry_show_latin_1_contents
246 246 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
247 247 [27, '27', '7bbf4c738e71'].each do |r1|
248 248 get :entry, :id => PRJ_ID,
249 249 :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
250 250 :rev => r1
251 251 assert_response :success
252 252 assert_template 'entry'
253 253 assert_select 'tr#L1 td.line-code', :text => /test-#{@char_1}.txt/
254 254 end
255 255 end
256 256 end
257 257
258 258 def test_entry_download
259 259 get :entry, :id => PRJ_ID,
260 260 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
261 261 :format => 'raw'
262 262 assert_response :success
263 263 # File content
264 264 assert @response.body.include?('WITHOUT ANY WARRANTY')
265 265 end
266 266
267 267 def test_entry_binary_force_download
268 268 get :entry, :id => PRJ_ID, :rev => 1,
269 269 :path => repository_path_hash(['images', 'edit.png'])[:param]
270 270 assert_response :success
271 271 assert_equal 'image/png', @response.content_type
272 272 end
273 273
274 274 def test_directory_entry
275 275 get :entry, :id => PRJ_ID,
276 276 :path => repository_path_hash(['sources'])[:param]
277 277 assert_response :success
278 278 assert_template 'show'
279 279 assert_not_nil assigns(:entry)
280 280 assert_equal 'sources', assigns(:entry).name
281 281 end
282 282
283 283 def test_diff
284 284 assert_equal 0, @repository.changesets.count
285 285 @repository.fetch_changesets
286 286 @project.reload
287 287 assert_equal NUM_REV, @repository.changesets.count
288 288 [4, '4', 'def6d2f1254a'].each do |r1|
289 289 # Full diff of changeset 4
290 290 ['inline', 'sbs'].each do |dt|
291 291 get :diff, :id => PRJ_ID, :rev => r1, :type => dt
292 292 assert_response :success
293 293 assert_template 'diff'
294 294 if @diff_c_support
295 295 # Line 22 removed
296 assert_select 'th.line-num:content(22) ~ td.diff_out', :text => /def remove/
296 assert_select 'th.line-num:contains(22) ~ td.diff_out', :text => /def remove/
297 297 assert_select 'h2', :text => /4:def6d2f1254a/
298 298 end
299 299 end
300 300 end
301 301 end
302 302
303 303 def test_diff_two_revs
304 304 assert_equal 0, @repository.changesets.count
305 305 @repository.fetch_changesets
306 306 @project.reload
307 307 assert_equal NUM_REV, @repository.changesets.count
308 308 [2, '400bb8672109', '400', 400].each do |r1|
309 309 [4, 'def6d2f1254a'].each do |r2|
310 310 ['inline', 'sbs'].each do |dt|
311 311 get :diff,
312 312 :id => PRJ_ID,
313 313 :rev => r1,
314 314 :rev_to => r2,
315 315 :type => dt
316 316 assert_response :success
317 317 assert_template 'diff'
318 318 diff = assigns(:diff)
319 319 assert_not_nil diff
320 320 assert_select 'h2', :text => /4:def6d2f1254a 2:400bb8672109/
321 321 end
322 322 end
323 323 end
324 324 end
325 325
326 326 def test_diff_latin_1_path
327 327 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
328 328 [21, 'adf805632193'].each do |r1|
329 329 ['inline', 'sbs'].each do |dt|
330 330 get :diff, :id => PRJ_ID, :rev => r1, :type => dt
331 331 assert_response :success
332 332 assert_template 'diff'
333 333 assert_select 'table' do
334 334 assert_select 'thead th.filename', :text => /latin-1-dir\/test-#{@char_1}-2.txt/
335 335 assert_select 'tbody td.diff_in', :text => /It is written in Python/
336 336 end
337 337 end
338 338 end
339 339 end
340 340 end
341 341
342 342 def test_diff_should_show_modified_filenames
343 343 get :diff, :id => PRJ_ID, :rev => '400bb8672109', :type => 'inline'
344 344 assert_response :success
345 345 assert_template 'diff'
346 346 assert_select 'th.filename', :text => 'sources/watchers_controller.rb'
347 347 end
348 348
349 349 def test_diff_should_show_deleted_filenames
350 350 get :diff, :id => PRJ_ID, :rev => 'b3a615152df8', :type => 'inline'
351 351 assert_response :success
352 352 assert_template 'diff'
353 353 assert_select 'th.filename', :text => 'sources/welcome_controller.rb'
354 354 end
355 355
356 356 def test_annotate
357 357 get :annotate, :id => PRJ_ID,
358 358 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
359 359 assert_response :success
360 360 assert_template 'annotate'
361 361
362 362 # Line 22, revision 4:def6d2f1254a
363 363 assert_select 'tr' do
364 364 assert_select 'th.line-num', :text => '22'
365 365 assert_select 'td.revision', :text => '4:def6d2f1254a'
366 366 assert_select 'td.author', :text => 'jsmith'
367 367 assert_select 'td', :text => /remove_watcher/
368 368 end
369 369 end
370 370
371 371 def test_annotate_not_in_tip
372 372 assert_equal 0, @repository.changesets.count
373 373 @repository.fetch_changesets
374 374 @project.reload
375 375 assert_equal NUM_REV, @repository.changesets.count
376 376 get :annotate, :id => PRJ_ID,
377 377 :path => repository_path_hash(['sources', 'welcome_controller.rb'])[:param]
378 378 assert_response 404
379 379 assert_select_error /was not found/
380 380 end
381 381
382 382 def test_annotate_at_given_revision
383 383 assert_equal 0, @repository.changesets.count
384 384 @repository.fetch_changesets
385 385 @project.reload
386 386 assert_equal NUM_REV, @repository.changesets.count
387 387 [2, '400bb8672109', '400', 400].each do |r1|
388 388 get :annotate, :id => PRJ_ID, :rev => r1,
389 389 :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
390 390 assert_response :success
391 391 assert_template 'annotate'
392 392 assert_select 'h2', :text => /@ 2:400bb8672109/
393 393 end
394 394 end
395 395
396 396 def test_annotate_latin_1_path
397 397 [21, '21', 'adf805632193'].each do |r1|
398 398 get :annotate, :id => PRJ_ID,
399 399 :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param],
400 400 :rev => r1
401 401 assert_response :success
402 402 assert_template 'annotate'
403 403 assert_select "th.line-num", :text => '1' do
404 404 assert_select "+ td.revision" do
405 405 assert_select "a", :text => '20:709858aafd1b'
406 406 assert_select "+ td.author", :text => "jsmith" do
407 407 assert_select "+ td",
408 408 :text => "Mercurial is a distributed version control system."
409 409 end
410 410 end
411 411 end
412 412 end
413 413 end
414 414
415 415 def test_annotate_latin_1_contents
416 416 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
417 417 [27, '7bbf4c738e71'].each do |r1|
418 418 get :annotate, :id => PRJ_ID,
419 419 :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
420 420 :rev => r1
421 421 assert_select 'tr#L1 td.line-code', :text => /test-#{@char_1}.txt/
422 422 end
423 423 end
424 424 end
425 425
426 426 def test_revision
427 427 assert_equal 0, @repository.changesets.count
428 428 @repository.fetch_changesets
429 429 @project.reload
430 430 assert_equal NUM_REV, @repository.changesets.count
431 431 ['1', '9d5b5b', '9d5b5b004199'].each do |r|
432 432 with_settings :default_language => "en" do
433 433 get :revision, :id => PRJ_ID, :rev => r
434 434 assert_response :success
435 435 assert_template 'revision'
436 436 assert_select 'title',
437 437 :text => 'Revision 1:9d5b5b004199 - Added 2 files and modified one. - eCookbook Subproject 1 - Redmine'
438 438 end
439 439 end
440 440 end
441 441
442 442 def test_empty_revision
443 443 assert_equal 0, @repository.changesets.count
444 444 @repository.fetch_changesets
445 445 @project.reload
446 446 assert_equal NUM_REV, @repository.changesets.count
447 447 ['', ' ', nil].each do |r|
448 448 get :revision, :id => PRJ_ID, :rev => r
449 449 assert_response 404
450 450 assert_select_error /was not found/
451 451 end
452 452 end
453 453
454 454 def test_destroy_valid_repository
455 455 @request.session[:user_id] = 1 # admin
456 456 assert_equal 0, @repository.changesets.count
457 457 @repository.fetch_changesets
458 458 assert_equal NUM_REV, @repository.changesets.count
459 459
460 460 assert_difference 'Repository.count', -1 do
461 461 delete :destroy, :id => @repository.id
462 462 end
463 463 assert_response 302
464 464 @project.reload
465 465 assert_nil @project.repository
466 466 end
467 467
468 468 def test_destroy_invalid_repository
469 469 @request.session[:user_id] = 1 # admin
470 470 @project.repository.destroy
471 471 @repository = Repository::Mercurial.create!(
472 472 :project => Project.find(PRJ_ID),
473 473 :url => "/invalid",
474 474 :path_encoding => 'ISO-8859-1'
475 475 )
476 476 @repository.fetch_changesets
477 477 assert_equal 0, @repository.changesets.count
478 478
479 479 assert_difference 'Repository.count', -1 do
480 480 delete :destroy, :id => @repository.id
481 481 end
482 482 assert_response 302
483 483 @project.reload
484 484 assert_nil @project.repository
485 485 end
486 486 else
487 487 puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!"
488 488 def test_fake; assert true end
489 489 end
490 490 end
@@ -1,332 +1,332
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class SearchControllerTest < ActionController::TestCase
21 21 fixtures :projects, :projects_trackers,
22 22 :enabled_modules, :roles, :users, :members, :member_roles,
23 23 :issues, :trackers, :issue_statuses, :enumerations,
24 24 :workflows,
25 25 :custom_fields, :custom_values,
26 26 :custom_fields_projects, :custom_fields_trackers,
27 27 :repositories, :changesets
28 28
29 29 def setup
30 30 User.current = nil
31 31 end
32 32
33 33 def test_search_for_projects
34 34 get :index
35 35 assert_response :success
36 36 assert_template 'index'
37 37
38 38 get :index, :q => "cook"
39 39 assert_response :success
40 40 assert_template 'index'
41 41 assert assigns(:results).include?(Project.find(1))
42 42 end
43 43
44 44 def test_search_on_archived_project_should_return_404
45 45 Project.find(3).archive
46 46 get :index, :id => 3
47 47 assert_response 404
48 48 end
49 49
50 50 def test_search_on_invisible_project_by_user_should_be_denied
51 51 @request.session[:user_id] = 7
52 52 get :index, :id => 2
53 53 assert_response 403
54 54 end
55 55
56 56 def test_search_on_invisible_project_by_anonymous_user_should_redirect
57 57 get :index, :id => 2
58 58 assert_response 302
59 59 end
60 60
61 61 def test_search_on_private_project_by_member_should_succeed
62 62 @request.session[:user_id] = 2
63 63 get :index, :id => 2
64 64 assert_response :success
65 65 end
66 66
67 67 def test_search_all_projects
68 68 with_settings :default_language => 'en' do
69 69 get :index, :q => 'recipe subproject commit', :all_words => ''
70 70 end
71 71 assert_response :success
72 72 assert_template 'index'
73 73
74 74 assert assigns(:results).include?(Issue.find(2))
75 75 assert assigns(:results).include?(Issue.find(5))
76 76 assert assigns(:results).include?(Changeset.find(101))
77 77 assert_select 'dt.issue a', :text => /Add ingredients categories/
78 78 assert_select 'dd', :text => /should be classified by categories/
79 79
80 80 assert assigns(:result_count_by_type).is_a?(Hash)
81 81 assert_equal 5, assigns(:result_count_by_type)['changesets']
82 82 assert_select 'a', :text => 'Changesets (5)'
83 83 end
84 84
85 85 def test_search_issues
86 86 get :index, :q => 'issue', :issues => 1
87 87 assert_response :success
88 88 assert_template 'index'
89 89
90 90 assert_equal true, assigns(:all_words)
91 91 assert_equal false, assigns(:titles_only)
92 92 assert assigns(:results).include?(Issue.find(8))
93 93 assert assigns(:results).include?(Issue.find(5))
94 94 assert_select 'dt.issue.closed a', :text => /Closed/
95 95 end
96 96
97 97 def test_search_issues_should_search_notes
98 98 Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
99 99
100 100 get :index, :q => 'searchkeyword', :issues => 1
101 101 assert_response :success
102 102 assert_include Issue.find(2), assigns(:results)
103 103 end
104 104
105 105 def test_search_issues_with_multiple_matches_in_journals_should_return_issue_once
106 106 Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
107 107 Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
108 108
109 109 get :index, :q => 'searchkeyword', :issues => 1
110 110 assert_response :success
111 111 assert_include Issue.find(2), assigns(:results)
112 112 assert_equal 1, assigns(:results).size
113 113 end
114 114
115 115 def test_search_issues_should_search_private_notes_with_permission_only
116 116 Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true)
117 117 @request.session[:user_id] = 2
118 118
119 119 Role.find(1).add_permission! :view_private_notes
120 120 get :index, :q => 'searchkeyword', :issues => 1
121 121 assert_response :success
122 122 assert_include Issue.find(2), assigns(:results)
123 123
124 124 Role.find(1).remove_permission! :view_private_notes
125 125 get :index, :q => 'searchkeyword', :issues => 1
126 126 assert_response :success
127 127 assert_not_include Issue.find(2), assigns(:results)
128 128 end
129 129
130 130 def test_search_all_projects_with_scope_param
131 131 get :index, :q => 'issue', :scope => 'all'
132 132 assert_response :success
133 133 assert_template 'index'
134 134 assert assigns(:results).present?
135 135 end
136 136
137 137 def test_search_my_projects
138 138 @request.session[:user_id] = 2
139 139 get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
140 140 assert_response :success
141 141 assert_template 'index'
142 142 assert assigns(:results).include?(Issue.find(1))
143 143 assert !assigns(:results).include?(Issue.find(5))
144 144 end
145 145
146 146 def test_search_my_projects_without_memberships
147 147 # anonymous user has no memberships
148 148 get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
149 149 assert_response :success
150 150 assert_template 'index'
151 151 assert assigns(:results).empty?
152 152 end
153 153
154 154 def test_search_project_and_subprojects
155 155 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => ''
156 156 assert_response :success
157 157 assert_template 'index'
158 158 assert assigns(:results).include?(Issue.find(1))
159 159 assert assigns(:results).include?(Issue.find(5))
160 160 end
161 161
162 162 def test_search_without_searchable_custom_fields
163 163 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
164 164
165 165 get :index, :id => 1
166 166 assert_response :success
167 167 assert_template 'index'
168 168 assert_not_nil assigns(:project)
169 169
170 170 get :index, :id => 1, :q => "can"
171 171 assert_response :success
172 172 assert_template 'index'
173 173 end
174 174
175 175 def test_search_with_searchable_custom_fields
176 176 get :index, :id => 1, :q => "stringforcustomfield"
177 177 assert_response :success
178 178 results = assigns(:results)
179 179 assert_not_nil results
180 180 assert_equal 1, results.size
181 181 assert results.include?(Issue.find(7))
182 182 end
183 183
184 184 def test_search_without_attachments
185 185 issue = Issue.generate! :subject => 'search_attachments'
186 186 attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
187 187
188 188 get :index, :id => 1, :q => 'search_attachments', :attachments => '0'
189 189 results = assigns(:results)
190 190 assert_equal 1, results.size
191 191 assert_equal issue, results.first
192 192 end
193 193
194 194 def test_search_attachments_only
195 195 issue = Issue.generate! :subject => 'search_attachments'
196 196 attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
197 197
198 198 get :index, :id => 1, :q => 'search_attachments', :attachments => 'only'
199 199 results = assigns(:results)
200 200 assert_equal 1, results.size
201 201 assert_equal attachment.container, results.first
202 202 end
203 203
204 204 def test_search_with_attachments
205 205 Issue.generate! :subject => 'search_attachments'
206 206 Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
207 207
208 208 get :index, :id => 1, :q => 'search_attachments', :attachments => '1'
209 209 results = assigns(:results)
210 210 assert_equal 2, results.size
211 211 end
212 212
213 213 def test_search_open_issues
214 214 Issue.generate! :subject => 'search_open'
215 215 Issue.generate! :subject => 'search_open', :status_id => 5
216 216
217 217 get :index, :id => 1, :q => 'search_open', :open_issues => '1'
218 218 results = assigns(:results)
219 219 assert_equal 1, results.size
220 220 end
221 221
222 222 def test_search_all_words
223 223 # 'all words' is on by default
224 224 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
225 225 assert_equal true, assigns(:all_words)
226 226 results = assigns(:results)
227 227 assert_not_nil results
228 228 assert_equal 1, results.size
229 229 assert results.include?(Issue.find(3))
230 230 end
231 231
232 232 def test_search_one_of_the_words
233 233 get :index, :id => 1, :q => 'recipe updating saving', :all_words => ''
234 234 assert_equal false, assigns(:all_words)
235 235 results = assigns(:results)
236 236 assert_not_nil results
237 237 assert_equal 3, results.size
238 238 assert results.include?(Issue.find(3))
239 239 end
240 240
241 241 def test_search_titles_only_without_result
242 242 get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1'
243 243 results = assigns(:results)
244 244 assert_not_nil results
245 245 assert_equal 0, results.size
246 246 end
247 247
248 248 def test_search_titles_only
249 249 get :index, :id => 1, :q => 'recipe', :titles_only => '1'
250 250 assert_equal true, assigns(:titles_only)
251 251 results = assigns(:results)
252 252 assert_not_nil results
253 253 assert_equal 2, results.size
254 254 end
255 255
256 256 def test_search_content
257 257 Issue.where(:id => 1).update_all("description = 'This is a searchkeywordinthecontent'")
258 258 get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => ''
259 259 assert_equal false, assigns(:titles_only)
260 260 results = assigns(:results)
261 261 assert_not_nil results
262 262 assert_equal 1, results.size
263 263 end
264 264
265 265 def test_search_with_pagination
266 266 issue = (0..24).map {Issue.generate! :subject => 'search_with_limited_results'}.reverse
267 267
268 268 get :index, :q => 'search_with_limited_results'
269 269 assert_response :success
270 270 assert_equal issue[0..9], assigns(:results)
271 271
272 272 get :index, :q => 'search_with_limited_results', :page => 2
273 273 assert_response :success
274 274 assert_equal issue[10..19], assigns(:results)
275 275
276 276 get :index, :q => 'search_with_limited_results', :page => 3
277 277 assert_response :success
278 278 assert_equal issue[20..24], assigns(:results)
279 279
280 280 get :index, :q => 'search_with_limited_results', :page => 4
281 281 assert_response :success
282 282 assert_equal [], assigns(:results)
283 283 end
284 284
285 285 def test_search_with_invalid_project_id
286 286 get :index, :id => 195, :q => 'recipe'
287 287 assert_response 404
288 288 assert_nil assigns(:results)
289 289 end
290 290
291 291 def test_quick_jump_to_issue
292 292 # issue of a public project
293 293 get :index, :q => "3"
294 294 assert_redirected_to '/issues/3'
295 295
296 296 # issue of a private project
297 297 get :index, :q => "4"
298 298 assert_response :success
299 299 assert_template 'index'
300 300 end
301 301
302 302 def test_large_integer
303 303 get :index, :q => '4615713488'
304 304 assert_response :success
305 305 assert_template 'index'
306 306 end
307 307
308 308 def test_tokens_with_quotes
309 309 get :index, :id => 1, :q => '"good bye" hello "bye bye"'
310 310 assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
311 311 end
312 312
313 313 def test_results_should_be_escaped_once
314 314 assert Issue.find(1).update_attributes(:subject => '<subject> escaped_once', :description => '<description> escaped_once')
315 315 get :index, :q => 'escaped_once'
316 316 assert_response :success
317 317 assert_select '#search-results' do
318 assert_select 'dt.issue a', :text => /&lt;subject&gt;/
319 assert_select 'dd', :text => /&lt;description&gt;/
318 assert_select 'dt.issue a', :text => /<subject>/
319 assert_select 'dd', :text => /<description>/
320 320 end
321 321 end
322 322
323 323 def test_keywords_should_be_highlighted
324 324 assert Issue.find(1).update_attributes(:subject => 'subject highlighted', :description => 'description highlighted')
325 325 get :index, :q => 'highlighted'
326 326 assert_response :success
327 327 assert_select '#search-results' do
328 328 assert_select 'dt.issue a span.highlight', :text => 'highlighted'
329 329 assert_select 'dd span.highlight', :text => 'highlighted'
330 330 end
331 331 end
332 332 end
@@ -1,192 +1,192
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class SettingsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :users
23 23
24 24 def setup
25 25 User.current = nil
26 26 @request.session[:user_id] = 1 # admin
27 27 end
28 28
29 29 def test_index
30 30 get :index
31 31 assert_response :success
32 32 assert_template 'edit'
33 33 end
34 34
35 35 def test_get_edit
36 36 get :edit
37 37 assert_response :success
38 38 assert_template 'edit'
39 39
40 40 assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]'
41 41 end
42 42
43 43 def test_get_edit_should_preselect_default_issue_list_columns
44 44 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
45 45 get :edit
46 46 assert_response :success
47 47 end
48 48
49 49 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
50 50 assert_select 'option', 4
51 51 assert_select 'option[value=tracker]', :text => 'Tracker'
52 52 assert_select 'option[value=subject]', :text => 'Subject'
53 53 assert_select 'option[value=status]', :text => 'Status'
54 54 assert_select 'option[value=updated_on]', :text => 'Updated'
55 55 end
56 56
57 57 assert_select 'select[id=available_columns]' do
58 58 assert_select 'option[value=tracker]', 0
59 59 assert_select 'option[value=priority]', :text => 'Priority'
60 60 end
61 61 end
62 62
63 63 def test_get_edit_without_trackers_should_succeed
64 64 Tracker.delete_all
65 65
66 66 get :edit
67 67 assert_response :success
68 68 end
69 69
70 70 def test_post_edit_notifications
71 71 post :edit, :settings => {:mail_from => 'functional@test.foo',
72 72 :bcc_recipients => '0',
73 73 :notified_events => %w(issue_added issue_updated news_added),
74 74 :emails_footer => 'Test footer'
75 75 }
76 76 assert_redirected_to '/settings'
77 77 assert_equal 'functional@test.foo', Setting.mail_from
78 78 assert !Setting.bcc_recipients?
79 79 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
80 80 assert_equal 'Test footer', Setting.emails_footer
81 81 Setting.clear_cache
82 82 end
83 83
84 84 def test_edit_commit_update_keywords
85 85 with_settings :commit_update_keywords => [
86 86 {"keywords" => "fixes, resolves", "status_id" => "3"},
87 87 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
88 88 ] do
89 89 get :edit
90 90 end
91 91 assert_response :success
92 92 assert_select 'tr.commit-keywords', 2
93 93 assert_select 'tr.commit-keywords:nth-child(1)' do
94 94 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
95 95 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
96 96 assert_select 'option[value="3"][selected=selected]'
97 97 end
98 98 end
99 99 assert_select 'tr.commit-keywords:nth-child(2)' do
100 100 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
101 101 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
102 102 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
103 103 end
104 104 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
105 105 assert_select 'option[value="100"][selected=selected]', :text => '100 %'
106 106 end
107 107 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
108 108 assert_select 'option[value="2"][selected=selected]', :text => 'Feature request'
109 109 end
110 110 end
111 111 end
112 112
113 113 def test_edit_without_commit_update_keywords_should_show_blank_line
114 114 with_settings :commit_update_keywords => [] do
115 115 get :edit
116 116 end
117 117 assert_response :success
118 118 assert_select 'tr.commit-keywords', 1 do
119 119 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
120 120 end
121 121 end
122 122
123 123 def test_post_edit_commit_update_keywords
124 124 post :edit, :settings => {
125 125 :commit_update_keywords => {
126 126 :keywords => ["resolves", "closes"],
127 127 :status_id => ["3", "5"],
128 128 :done_ratio => ["", "100"],
129 129 :if_tracker_id => ["", "2"]
130 130 }
131 131 }
132 132 assert_redirected_to '/settings'
133 133 assert_equal([
134 134 {"keywords" => "resolves", "status_id" => "3"},
135 135 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
136 136 ], Setting.commit_update_keywords)
137 137 end
138 138
139 139 def test_get_plugin_settings
140 140 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
141 141 Redmine::Plugin.register :foo do
142 settings :partial => "foo_plugin/foo_plugin_settings",
143 :default => {'sample_setting' => 'Plugin setting value'}
142 settings :partial => "foo_plugin/foo_plugin_settings"
144 143 end
144 Setting.plugin_foo = {'sample_setting' => 'Plugin setting value'}
145 145
146 146 get :plugin, :id => 'foo'
147 147 assert_response :success
148 148 assert_template 'plugin'
149 149 assert_select 'form[action="/settings/plugin/foo"]' do
150 150 assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value'
151 151 end
152 152 ensure
153 153 Redmine::Plugin.unregister(:foo)
154 154 end
155 155
156 156 def test_get_invalid_plugin_settings
157 157 get :plugin, :id => 'none'
158 158 assert_response 404
159 159 end
160 160
161 161 def test_get_non_configurable_plugin_settings
162 162 Redmine::Plugin.register(:foo) {}
163 163
164 164 get :plugin, :id => 'foo'
165 165 assert_response 404
166 166
167 167 ensure
168 168 Redmine::Plugin.unregister(:foo)
169 169 end
170 170
171 171 def test_post_plugin_settings
172 172 Redmine::Plugin.register(:foo) do
173 173 settings :partial => 'not blank', # so that configurable? is true
174 174 :default => {'sample_setting' => 'Plugin setting value'}
175 175 end
176 176
177 177 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
178 178 assert_redirected_to '/settings/plugin/foo'
179 179
180 180 assert_equal({'sample_setting' => 'Value'}, Setting.plugin_foo)
181 181 end
182 182
183 183 def test_post_non_configurable_plugin_settings
184 184 Redmine::Plugin.register(:foo) {}
185 185
186 186 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
187 187 assert_response 404
188 188
189 189 ensure
190 190 Redmine::Plugin.unregister(:foo)
191 191 end
192 192 end
@@ -1,723 +1,723
1 1 # -*- coding: utf-8 -*-
2 2 # Redmine - project management software
3 3 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 4 #
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; either version 2
8 8 # of the License, or (at your option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 18
19 19 require File.expand_path('../../test_helper', __FILE__)
20 20
21 21 class TimelogControllerTest < ActionController::TestCase
22 22 fixtures :projects, :enabled_modules, :roles, :members,
23 23 :member_roles, :issues, :time_entries, :users,
24 24 :trackers, :enumerations, :issue_statuses,
25 25 :custom_fields, :custom_values,
26 26 :projects_trackers, :custom_fields_trackers,
27 27 :custom_fields_projects
28 28
29 29 include Redmine::I18n
30 30
31 31 def test_new
32 32 @request.session[:user_id] = 3
33 33 get :new
34 34 assert_response :success
35 35 assert_template 'new'
36 36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 39 # blank option for project
40 40 assert_select 'option[value=""]'
41 41 end
42 42 end
43 43
44 44 def test_new_with_project_id
45 45 @request.session[:user_id] = 3
46 46 get :new, :project_id => 1
47 47 assert_response :success
48 48 assert_template 'new'
49 49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
51 51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
52 52 end
53 53
54 54 def test_new_with_issue_id
55 55 @request.session[:user_id] = 3
56 56 get :new, :issue_id => 2
57 57 assert_response :success
58 58 assert_template 'new'
59 59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
60 60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
62 62 end
63 63
64 64 def test_new_without_project_should_prefill_the_form
65 65 @request.session[:user_id] = 3
66 66 get :new, :time_entry => {:project_id => '1'}
67 67 assert_response :success
68 68 assert_template 'new'
69 69 assert_select 'select[name=?]', 'time_entry[project_id]' do
70 70 assert_select 'option[value="1"][selected=selected]'
71 71 end
72 72 end
73 73
74 74 def test_new_without_project_should_deny_without_permission
75 75 Role.all.each {|role| role.remove_permission! :log_time}
76 76 @request.session[:user_id] = 3
77 77
78 78 get :new
79 79 assert_response 403
80 80 end
81 81
82 82 def test_new_should_select_default_activity
83 83 @request.session[:user_id] = 3
84 84 get :new, :project_id => 1
85 85 assert_response :success
86 86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
87 87 assert_select 'option[selected=selected]', :text => 'Development'
88 88 end
89 89 end
90 90
91 91 def test_new_should_only_show_active_time_entry_activities
92 92 @request.session[:user_id] = 3
93 93 get :new, :project_id => 1
94 94 assert_response :success
95 95 assert_select 'option', :text => 'Inactive Activity', :count => 0
96 96 end
97 97
98 98 def test_get_edit_existing_time
99 99 @request.session[:user_id] = 2
100 100 get :edit, :id => 2, :project_id => nil
101 101 assert_response :success
102 102 assert_template 'edit'
103 103 assert_select 'form[action=?]', '/time_entries/2'
104 104 end
105 105
106 106 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
107 107 te = TimeEntry.find(1)
108 108 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
109 109 te.save!
110 110
111 111 @request.session[:user_id] = 1
112 112 get :edit, :project_id => 1, :id => 1
113 113 assert_response :success
114 114 assert_template 'edit'
115 115 # Blank option since nothing is pre-selected
116 116 assert_select 'option', :text => '--- Please select ---'
117 117 end
118 118
119 119 def test_post_create
120 120 @request.session[:user_id] = 3
121 121 assert_difference 'TimeEntry.count' do
122 122 post :create, :project_id => 1,
123 123 :time_entry => {:comments => 'Some work on TimelogControllerTest',
124 124 # Not the default activity
125 125 :activity_id => '11',
126 126 :spent_on => '2008-03-14',
127 127 :issue_id => '1',
128 128 :hours => '7.3'}
129 129 assert_redirected_to '/projects/ecookbook/time_entries'
130 130 end
131 131
132 132 t = TimeEntry.order('id DESC').first
133 133 assert_not_nil t
134 134 assert_equal 'Some work on TimelogControllerTest', t.comments
135 135 assert_equal 1, t.project_id
136 136 assert_equal 1, t.issue_id
137 137 assert_equal 11, t.activity_id
138 138 assert_equal 7.3, t.hours
139 139 assert_equal 3, t.user_id
140 140 end
141 141
142 142 def test_post_create_with_blank_issue
143 143 @request.session[:user_id] = 3
144 144 assert_difference 'TimeEntry.count' do
145 145 post :create, :project_id => 1,
146 146 :time_entry => {:comments => 'Some work on TimelogControllerTest',
147 147 # Not the default activity
148 148 :activity_id => '11',
149 149 :issue_id => '',
150 150 :spent_on => '2008-03-14',
151 151 :hours => '7.3'}
152 152 assert_redirected_to '/projects/ecookbook/time_entries'
153 153 end
154 154
155 155 t = TimeEntry.order('id DESC').first
156 156 assert_not_nil t
157 157 assert_equal 'Some work on TimelogControllerTest', t.comments
158 158 assert_equal 1, t.project_id
159 159 assert_nil t.issue_id
160 160 assert_equal 11, t.activity_id
161 161 assert_equal 7.3, t.hours
162 162 assert_equal 3, t.user_id
163 163 end
164 164
165 165 def test_create_and_continue_at_project_level
166 166 @request.session[:user_id] = 2
167 167 assert_difference 'TimeEntry.count' do
168 168 post :create, :time_entry => {:project_id => '1',
169 169 :activity_id => '11',
170 170 :issue_id => '',
171 171 :spent_on => '2008-03-14',
172 172 :hours => '7.3'},
173 173 :continue => '1'
174 174 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
175 175 end
176 176 end
177 177
178 178 def test_create_and_continue_at_issue_level
179 179 @request.session[:user_id] = 2
180 180 assert_difference 'TimeEntry.count' do
181 181 post :create, :time_entry => {:project_id => '',
182 182 :activity_id => '11',
183 183 :issue_id => '1',
184 184 :spent_on => '2008-03-14',
185 185 :hours => '7.3'},
186 186 :continue => '1'
187 187 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
188 188 end
189 189 end
190 190
191 191 def test_create_and_continue_with_project_id
192 192 @request.session[:user_id] = 2
193 193 assert_difference 'TimeEntry.count' do
194 194 post :create, :project_id => 1,
195 195 :time_entry => {:activity_id => '11',
196 196 :issue_id => '',
197 197 :spent_on => '2008-03-14',
198 198 :hours => '7.3'},
199 199 :continue => '1'
200 200 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
201 201 end
202 202 end
203 203
204 204 def test_create_and_continue_with_issue_id
205 205 @request.session[:user_id] = 2
206 206 assert_difference 'TimeEntry.count' do
207 207 post :create, :issue_id => 1,
208 208 :time_entry => {:activity_id => '11',
209 209 :issue_id => '1',
210 210 :spent_on => '2008-03-14',
211 211 :hours => '7.3'},
212 212 :continue => '1'
213 213 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
214 214 end
215 215 end
216 216
217 217 def test_create_without_log_time_permission_should_be_denied
218 218 @request.session[:user_id] = 2
219 219 Role.find_by_name('Manager').remove_permission! :log_time
220 220 post :create, :project_id => 1,
221 221 :time_entry => {:activity_id => '11',
222 222 :issue_id => '',
223 223 :spent_on => '2008-03-14',
224 224 :hours => '7.3'}
225 225
226 226 assert_response 403
227 227 end
228 228
229 229 def test_create_without_project_and_issue_should_fail
230 230 @request.session[:user_id] = 2
231 231 post :create, :time_entry => {:issue_id => ''}
232 232
233 233 assert_response :success
234 234 assert_template 'new'
235 235 end
236 236
237 237 def test_create_with_failure
238 238 @request.session[:user_id] = 2
239 239 post :create, :project_id => 1,
240 240 :time_entry => {:activity_id => '',
241 241 :issue_id => '',
242 242 :spent_on => '2008-03-14',
243 243 :hours => '7.3'}
244 244
245 245 assert_response :success
246 246 assert_template 'new'
247 247 end
248 248
249 249 def test_create_without_project
250 250 @request.session[:user_id] = 2
251 251 assert_difference 'TimeEntry.count' do
252 252 post :create, :time_entry => {:project_id => '1',
253 253 :activity_id => '11',
254 254 :issue_id => '',
255 255 :spent_on => '2008-03-14',
256 256 :hours => '7.3'}
257 257 end
258 258
259 259 assert_redirected_to '/projects/ecookbook/time_entries'
260 260 time_entry = TimeEntry.order('id DESC').first
261 261 assert_equal 1, time_entry.project_id
262 262 end
263 263
264 264 def test_create_without_project_should_fail_with_issue_not_inside_project
265 265 @request.session[:user_id] = 2
266 266 assert_no_difference 'TimeEntry.count' do
267 267 post :create, :time_entry => {:project_id => '1',
268 268 :activity_id => '11',
269 269 :issue_id => '5',
270 270 :spent_on => '2008-03-14',
271 271 :hours => '7.3'}
272 272 end
273 273
274 274 assert_response :success
275 275 assert assigns(:time_entry).errors[:issue_id].present?
276 276 end
277 277
278 278 def test_create_without_project_should_deny_without_permission
279 279 @request.session[:user_id] = 2
280 280 Project.find(3).disable_module!(:time_tracking)
281 281
282 282 assert_no_difference 'TimeEntry.count' do
283 283 post :create, :time_entry => {:project_id => '3',
284 284 :activity_id => '11',
285 285 :issue_id => '',
286 286 :spent_on => '2008-03-14',
287 287 :hours => '7.3'}
288 288 end
289 289
290 290 assert_response 403
291 291 end
292 292
293 293 def test_create_without_project_with_failure
294 294 @request.session[:user_id] = 2
295 295 assert_no_difference 'TimeEntry.count' do
296 296 post :create, :time_entry => {:project_id => '1',
297 297 :activity_id => '11',
298 298 :issue_id => '',
299 299 :spent_on => '2008-03-14',
300 300 :hours => ''}
301 301 end
302 302
303 303 assert_response :success
304 304 assert_select 'select[name=?]', 'time_entry[project_id]' do
305 305 assert_select 'option[value="1"][selected=selected]'
306 306 end
307 307 end
308 308
309 309 def test_update
310 310 entry = TimeEntry.find(1)
311 311 assert_equal 1, entry.issue_id
312 312 assert_equal 2, entry.user_id
313 313
314 314 @request.session[:user_id] = 1
315 315 put :update, :id => 1,
316 316 :time_entry => {:issue_id => '2',
317 317 :hours => '8'}
318 318 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
319 319 entry.reload
320 320
321 321 assert_equal 8, entry.hours
322 322 assert_equal 2, entry.issue_id
323 323 assert_equal 2, entry.user_id
324 324 end
325 325
326 326 def test_update_should_allow_to_change_issue_to_another_project
327 327 entry = TimeEntry.generate!(:issue_id => 1)
328 328
329 329 @request.session[:user_id] = 1
330 330 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
331 331 assert_response 302
332 332 entry.reload
333 333
334 334 assert_equal 5, entry.issue_id
335 335 assert_equal 3, entry.project_id
336 336 end
337 337
338 338 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
339 339 entry = TimeEntry.generate!(:issue_id => 1)
340 340 Project.find(3).disable_module!(:time_tracking)
341 341
342 342 @request.session[:user_id] = 1
343 343 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
344 344 assert_response 200
345 345 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
346 346 end
347 347
348 348 def test_get_bulk_edit
349 349 @request.session[:user_id] = 2
350 350 get :bulk_edit, :ids => [1, 2]
351 351 assert_response :success
352 352 assert_template 'bulk_edit'
353 353
354 354 assert_select 'ul#bulk-selection' do
355 355 assert_select 'li', 2
356 356 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
357 357 end
358 358
359 359 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
360 360 # System wide custom field
361 361 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
362 362
363 363 # Activities
364 364 assert_select 'select[name=?]', 'time_entry[activity_id]' do
365 365 assert_select 'option[value=""]', :text => '(No change)'
366 366 assert_select 'option[value="9"]', :text => 'Design'
367 367 end
368 368 end
369 369 end
370 370
371 371 def test_get_bulk_edit_on_different_projects
372 372 @request.session[:user_id] = 2
373 373 get :bulk_edit, :ids => [1, 2, 6]
374 374 assert_response :success
375 375 assert_template 'bulk_edit'
376 376 end
377 377
378 378 def test_bulk_update
379 379 @request.session[:user_id] = 2
380 380 # update time entry activity
381 381 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
382 382
383 383 assert_response 302
384 384 # check that the issues were updated
385 385 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
386 386 end
387 387
388 388 def test_bulk_update_with_failure
389 389 @request.session[:user_id] = 2
390 390 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
391 391
392 392 assert_response 302
393 393 assert_match /Failed to save 2 time entrie/, flash[:error]
394 394 end
395 395
396 396 def test_bulk_update_on_different_projects
397 397 @request.session[:user_id] = 2
398 398 # makes user a manager on the other project
399 399 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
400 400
401 401 # update time entry activity
402 402 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
403 403
404 404 assert_response 302
405 405 # check that the issues were updated
406 406 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
407 407 end
408 408
409 409 def test_bulk_update_on_different_projects_without_rights
410 410 @request.session[:user_id] = 3
411 411 user = User.find(3)
412 412 action = { :controller => "timelog", :action => "bulk_update" }
413 413 assert user.allowed_to?(action, TimeEntry.find(1).project)
414 414 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
415 415 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
416 416 assert_response 403
417 417 end
418 418
419 419 def test_bulk_update_custom_field
420 420 @request.session[:user_id] = 2
421 421 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
422 422
423 423 assert_response 302
424 424 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
425 425 end
426 426
427 427 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
428 428 @request.session[:user_id] = 2
429 429 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
430 430
431 431 assert_response :redirect
432 432 assert_redirected_to '/time_entries'
433 433 end
434 434
435 435 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
436 436 @request.session[:user_id] = 2
437 437 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
438 438
439 439 assert_response :redirect
440 440 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
441 441 end
442 442
443 443 def test_post_bulk_update_without_edit_permission_should_be_denied
444 444 @request.session[:user_id] = 2
445 445 Role.find_by_name('Manager').remove_permission! :edit_time_entries
446 446 post :bulk_update, :ids => [1,2]
447 447
448 448 assert_response 403
449 449 end
450 450
451 451 def test_destroy
452 452 @request.session[:user_id] = 2
453 453 delete :destroy, :id => 1
454 454 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
455 455 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
456 456 assert_nil TimeEntry.find_by_id(1)
457 457 end
458 458
459 459 def test_destroy_should_fail
460 460 # simulate that this fails (e.g. due to a plugin), see #5700
461 461 TimeEntry.any_instance.expects(:destroy).returns(false)
462 462
463 463 @request.session[:user_id] = 2
464 464 delete :destroy, :id => 1
465 465 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
466 466 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
467 467 assert_not_nil TimeEntry.find_by_id(1)
468 468 end
469 469
470 470 def test_index_all_projects
471 471 get :index
472 472 assert_response :success
473 473 assert_template 'index'
474 474 assert_not_nil assigns(:total_hours)
475 475 assert_equal "162.90", "%.2f" % assigns(:total_hours)
476 476 assert_select 'form#query_form[action=?]', '/time_entries'
477 477 end
478 478
479 479 def test_index_all_projects_should_show_log_time_link
480 480 @request.session[:user_id] = 2
481 481 get :index
482 482 assert_response :success
483 483 assert_template 'index'
484 484 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
485 485 end
486 486
487 487 def test_index_my_spent_time
488 488 @request.session[:user_id] = 2
489 489 get :index, :user_id => 'me'
490 490 assert_response :success
491 491 assert_template 'index'
492 492 assert assigns(:entries).all? {|entry| entry.user_id == 2}
493 493 end
494 494
495 495 def test_index_at_project_level
496 496 get :index, :project_id => 'ecookbook'
497 497 assert_response :success
498 498 assert_template 'index'
499 499 assert_not_nil assigns(:entries)
500 500 assert_equal 4, assigns(:entries).size
501 501 # project and subproject
502 502 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
503 503 assert_not_nil assigns(:total_hours)
504 504 assert_equal "162.90", "%.2f" % assigns(:total_hours)
505 505 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
506 506 end
507 507
508 508 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
509 509 entry = TimeEntry.generate!(:project => Project.find(3))
510 510
511 511 with_settings :display_subprojects_issues => '0' do
512 512 get :index, :project_id => 'ecookbook'
513 513 assert_response :success
514 514 assert_template 'index'
515 515 assert_not_include entry, assigns(:entries)
516 516 end
517 517 end
518 518
519 519 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
520 520 entry = TimeEntry.generate!(:project => Project.find(3))
521 521
522 522 with_settings :display_subprojects_issues => '0' do
523 523 get :index, :project_id => 'ecookbook', :subproject_id => 3
524 524 assert_response :success
525 525 assert_template 'index'
526 526 assert_include entry, assigns(:entries)
527 527 end
528 528 end
529 529
530 530 def test_index_at_project_level_with_date_range
531 531 get :index, :project_id => 'ecookbook',
532 532 :f => ['spent_on'],
533 533 :op => {'spent_on' => '><'},
534 534 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
535 535 assert_response :success
536 536 assert_template 'index'
537 537 assert_not_nil assigns(:entries)
538 538 assert_equal 3, assigns(:entries).size
539 539 assert_not_nil assigns(:total_hours)
540 540 assert_equal "12.90", "%.2f" % assigns(:total_hours)
541 541 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
542 542 end
543 543
544 544 def test_index_at_project_level_with_date_range_using_from_and_to_params
545 545 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
546 546 assert_response :success
547 547 assert_template 'index'
548 548 assert_not_nil assigns(:entries)
549 549 assert_equal 3, assigns(:entries).size
550 550 assert_not_nil assigns(:total_hours)
551 551 assert_equal "12.90", "%.2f" % assigns(:total_hours)
552 552 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
553 553 end
554 554
555 555 def test_index_at_project_level_with_period
556 556 get :index, :project_id => 'ecookbook',
557 557 :f => ['spent_on'],
558 558 :op => {'spent_on' => '>t-'},
559 559 :v => {'spent_on' => ['7']}
560 560 assert_response :success
561 561 assert_template 'index'
562 562 assert_not_nil assigns(:entries)
563 563 assert_not_nil assigns(:total_hours)
564 564 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
565 565 end
566 566
567 567 def test_index_at_issue_level
568 568 get :index, :issue_id => 1
569 569 assert_response :success
570 570 assert_template 'index'
571 571 assert_not_nil assigns(:entries)
572 572 assert_equal 2, assigns(:entries).size
573 573 assert_not_nil assigns(:total_hours)
574 574 assert_equal 154.25, assigns(:total_hours)
575 575 # display all time
576 576 assert_nil assigns(:from)
577 577 assert_nil assigns(:to)
578 578 assert_select 'form#query_form[action=?]', '/issues/1/time_entries'
579 579 end
580 580
581 581 def test_index_should_sort_by_spent_on_and_created_on
582 582 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
583 583 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
584 584 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
585 585
586 586 get :index, :project_id => 1,
587 587 :f => ['spent_on'],
588 588 :op => {'spent_on' => '><'},
589 589 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
590 590 assert_response :success
591 591 assert_equal [t2, t1, t3], assigns(:entries)
592 592
593 593 get :index, :project_id => 1,
594 594 :f => ['spent_on'],
595 595 :op => {'spent_on' => '><'},
596 596 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
597 597 :sort => 'spent_on'
598 598 assert_response :success
599 599 assert_equal [t3, t1, t2], assigns(:entries)
600 600 end
601 601
602 602 def test_index_with_filter_on_issue_custom_field
603 603 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
604 604 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
605 605
606 606 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
607 607 assert_response :success
608 608 assert_equal [entry], assigns(:entries)
609 609 end
610 610
611 611 def test_index_with_issue_custom_field_column
612 612 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
613 613 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
614 614
615 615 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
616 616 assert_response :success
617 617 assert_include :'issue.cf_2', assigns(:query).column_names
618 618 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
619 619 end
620 620
621 621 def test_index_with_time_entry_custom_field_column
622 622 field = TimeEntryCustomField.generate!(:field_format => 'string')
623 623 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
624 624 field_name = "cf_#{field.id}"
625 625
626 626 get :index, :c => ["hours", field_name]
627 627 assert_response :success
628 628 assert_include field_name.to_sym, assigns(:query).column_names
629 629 assert_select "td.#{field_name}", :text => 'CF Value'
630 630 end
631 631
632 632 def test_index_with_time_entry_custom_field_sorting
633 633 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
634 634 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
635 635 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
636 636 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
637 637 field_name = "cf_#{field.id}"
638 638
639 639 get :index, :c => ["hours", field_name], :sort => field_name
640 640 assert_response :success
641 641 assert_include field_name.to_sym, assigns(:query).column_names
642 642 assert_select "th a.sort", :text => 'String Field'
643 643
644 644 # Make sure that values are properly sorted
645 645 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
646 646 assert_equal 3, values.size
647 647 assert_equal values.sort, values
648 648 end
649 649
650 650 def test_index_atom_feed
651 651 get :index, :project_id => 1, :format => 'atom'
652 652 assert_response :success
653 653 assert_equal 'application/atom+xml', @response.content_type
654 654 assert_not_nil assigns(:items)
655 655 assert assigns(:items).first.is_a?(TimeEntry)
656 656 end
657 657
658 658 def test_index_at_project_level_should_include_csv_export_dialog
659 659 get :index, :project_id => 'ecookbook',
660 660 :f => ['spent_on'],
661 661 :op => {'spent_on' => '>='},
662 662 :v => {'spent_on' => ['2007-04-01']},
663 663 :c => ['spent_on', 'user']
664 664 assert_response :success
665 665
666 666 assert_select '#csv-export-options' do
667 667 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
668 668 # filter
669 669 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
670 assert_select 'input[name=?][value=?]', 'op[spent_on]', '&gt;='
670 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
671 671 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
672 672 # columns
673 673 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
674 674 assert_select 'input[name=?][value=?]', 'c[]', 'user'
675 675 assert_select 'input[name=?]', 'c[]', 2
676 676 end
677 677 end
678 678 end
679 679
680 680 def test_index_cross_project_should_include_csv_export_dialog
681 681 get :index
682 682 assert_response :success
683 683
684 684 assert_select '#csv-export-options' do
685 685 assert_select 'form[action=?][method=get]', '/time_entries.csv'
686 686 end
687 687 end
688 688
689 689 def test_index_at_issue_level_should_include_csv_export_dialog
690 690 get :index, :issue_id => 3
691 691 assert_response :success
692 692
693 693 assert_select '#csv-export-options' do
694 694 assert_select 'form[action=?][method=get]', '/issues/3/time_entries.csv'
695 695 end
696 696 end
697 697
698 698 def test_index_csv_all_projects
699 699 with_settings :date_format => '%m/%d/%Y' do
700 700 get :index, :format => 'csv'
701 701 assert_response :success
702 702 assert_equal 'text/csv; header=present', response.content_type
703 703 end
704 704 end
705 705
706 706 def test_index_csv
707 707 with_settings :date_format => '%m/%d/%Y' do
708 708 get :index, :project_id => 1, :format => 'csv'
709 709 assert_response :success
710 710 assert_equal 'text/csv; header=present', response.content_type
711 711 end
712 712 end
713 713
714 714 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
715 715 issue = Issue.find(1)
716 716 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
717 717
718 718 get :index, :format => 'csv'
719 719 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
720 720 assert_not_nil line
721 721 assert_include "#{issue.tracker} #1: #{issue.subject}", line
722 722 end
723 723 end
@@ -1,180 +1,180
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WelcomeControllerTest < ActionController::TestCase
21 21 fixtures :projects, :news, :users, :members
22 22
23 23 def setup
24 24 User.current = nil
25 25 end
26 26
27 27 def test_index
28 28 get :index
29 29 assert_response :success
30 30 assert_template 'index'
31 31 assert_not_nil assigns(:news)
32 32 assert_not_nil assigns(:projects)
33 33 assert !assigns(:projects).include?(Project.where(:is_public => false).first)
34 34 end
35 35
36 36 def test_browser_language
37 37 Setting.default_language = 'en'
38 38 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
39 39 get :index
40 40 assert_equal :fr, @controller.current_language
41 41 end
42 42
43 43 def test_browser_language_alternate
44 44 Setting.default_language = 'en'
45 45 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW'
46 46 get :index
47 47 assert_equal :"zh-TW", @controller.current_language
48 48 end
49 49
50 50 def test_browser_language_alternate_not_valid
51 51 Setting.default_language = 'en'
52 52 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA'
53 53 get :index
54 54 assert_equal :fr, @controller.current_language
55 55 end
56 56
57 57 def test_browser_language_should_be_ignored_with_force_default_language_for_anonymous
58 58 Setting.default_language = 'en'
59 59 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
60 60 with_settings :force_default_language_for_anonymous => '1' do
61 61 get :index
62 62 assert_equal :en, @controller.current_language
63 63 end
64 64 end
65 65
66 66 def test_user_language_should_be_used
67 67 Setting.default_language = 'fi'
68 68 user = User.find(2).update_attribute :language, 'it'
69 69 @request.session[:user_id] = 2
70 70 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
71 71 get :index
72 72 assert_equal :it, @controller.current_language
73 73 end
74 74
75 75 def test_user_language_should_be_ignored_if_force_default_language_for_loggedin
76 76 Setting.default_language = 'fi'
77 77 user = User.find(2).update_attribute :language, 'it'
78 78 @request.session[:user_id] = 2
79 79 @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
80 80 with_settings :force_default_language_for_loggedin => '1' do
81 81 get :index
82 82 assert_equal :fi, @controller.current_language
83 83 end
84 84 end
85 85
86 86 def test_robots
87 87 get :robots
88 88 assert_response :success
89 89 assert_equal 'text/plain', @response.content_type
90 90 assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues\r?$})
91 91 end
92 92
93 93 def test_warn_on_leaving_unsaved_turn_on
94 94 user = User.find(2)
95 95 user.pref.warn_on_leaving_unsaved = '1'
96 96 user.pref.save!
97 97 @request.session[:user_id] = 2
98 98
99 99 get :index
100 100 assert_select 'script', :text => %r{warnLeavingUnsaved}
101 101 end
102 102
103 103 def test_warn_on_leaving_unsaved_turn_off
104 104 user = User.find(2)
105 105 user.pref.warn_on_leaving_unsaved = '0'
106 106 user.pref.save!
107 107 @request.session[:user_id] = 2
108 108
109 109 get :index
110 110 assert_select 'script', :text => %r{warnLeavingUnsaved}, :count => 0
111 111 end
112 112
113 113 def test_logout_link_should_post
114 114 @request.session[:user_id] = 2
115 115
116 116 get :index
117 117 assert_select 'a[href="/logout"][data-method=post]', :text => 'Sign out'
118 118 end
119 119
120 120 def test_call_hook_mixed_in
121 121 assert @controller.respond_to?(:call_hook)
122 122 end
123 123
124 124 def test_project_jump_box_should_escape_names_once
125 125 Project.find(1).update_attribute :name, 'Foo & Bar'
126 126 @request.session[:user_id] = 2
127 127
128 128 get :index
129 129 assert_select "#header select" do
130 assert_select "option", :text => 'Foo &amp; Bar'
130 assert_select "option", :text => 'Foo & Bar'
131 131 end
132 132 end
133 133
134 134 def test_api_offset_and_limit_without_params
135 135 assert_equal [0, 25], @controller.api_offset_and_limit({})
136 136 end
137 137
138 138 def test_api_offset_and_limit_with_limit
139 139 assert_equal [0, 30], @controller.api_offset_and_limit({:limit => 30})
140 140 assert_equal [0, 100], @controller.api_offset_and_limit({:limit => 120})
141 141 assert_equal [0, 25], @controller.api_offset_and_limit({:limit => -10})
142 142 end
143 143
144 144 def test_api_offset_and_limit_with_offset
145 145 assert_equal [10, 25], @controller.api_offset_and_limit({:offset => 10})
146 146 assert_equal [0, 25], @controller.api_offset_and_limit({:offset => -10})
147 147 end
148 148
149 149 def test_api_offset_and_limit_with_offset_and_limit
150 150 assert_equal [10, 50], @controller.api_offset_and_limit({:offset => 10, :limit => 50})
151 151 end
152 152
153 153 def test_api_offset_and_limit_with_page
154 154 assert_equal [0, 25], @controller.api_offset_and_limit({:page => 1})
155 155 assert_equal [50, 25], @controller.api_offset_and_limit({:page => 3})
156 156 assert_equal [0, 25], @controller.api_offset_and_limit({:page => 0})
157 157 assert_equal [0, 25], @controller.api_offset_and_limit({:page => -2})
158 158 end
159 159
160 160 def test_api_offset_and_limit_with_page_and_limit
161 161 assert_equal [0, 100], @controller.api_offset_and_limit({:page => 1, :limit => 100})
162 162 assert_equal [200, 100], @controller.api_offset_and_limit({:page => 3, :limit => 100})
163 163 end
164 164
165 165 def test_unhautorized_exception_with_anonymous_should_redirect_to_login
166 166 WelcomeController.any_instance.stubs(:index).raises(::Unauthorized)
167 167
168 168 get :index
169 169 assert_response 302
170 170 assert_redirected_to('/login?back_url='+CGI.escape('http://test.host/'))
171 171 end
172 172
173 173 def test_unhautorized_exception_with_anonymous_and_xmlhttprequest_should_respond_with_401_to_anonymous
174 174 WelcomeController.any_instance.stubs(:index).raises(::Unauthorized)
175 175
176 176 @request.env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
177 177 get :index
178 178 assert_response 401
179 179 end
180 180 end
@@ -1,360 +1,360
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WorkflowsControllerTest < ActionController::TestCase
21 21 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'index'
32 32
33 33 count = WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count
34 assert_select 'a[href=?]', '/workflows/edit?role_id=1&amp;tracker_id=2', :content => count.to_s
34 assert_select 'a[href=?]', '/workflows/edit?role_id=1&tracker_id=2', :content => count.to_s
35 35 end
36 36
37 37 def test_get_edit
38 38 get :edit
39 39 assert_response :success
40 40 assert_template 'edit'
41 41 end
42 42
43 43 def test_get_edit_with_role_and_tracker
44 44 WorkflowTransition.delete_all
45 45 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
46 46 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
47 47
48 48 get :edit, :role_id => 2, :tracker_id => 1
49 49 assert_response :success
50 50 assert_template 'edit'
51 51
52 52 # used status only
53 53 assert_not_nil assigns(:statuses)
54 54 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
55 55
56 56 # allowed transitions
57 57 assert_select 'input[type=checkbox][name=?][value="1"][checked=checked]', 'transitions[3][5][always]'
58 58 # not allowed
59 59 assert_select 'input[type=checkbox][name=?][value="1"]:not([checked=checked])', 'transitions[3][2][always]'
60 60 # unused
61 61 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]', 0
62 62 end
63 63
64 64 def test_get_edit_with_all_roles_and_all_trackers
65 65 get :edit, :role_id => 'all', :tracker_id => 'all'
66 66 assert_response :success
67 67 assert_equal Role.sorted.to_a, assigns(:roles)
68 68 assert_equal Tracker.sorted.to_a, assigns(:trackers)
69 69 end
70 70
71 71 def test_get_edit_with_role_and_tracker_and_all_statuses
72 72 WorkflowTransition.delete_all
73 73
74 74 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
75 75 assert_response :success
76 76 assert_template 'edit'
77 77
78 78 assert_not_nil assigns(:statuses)
79 79 assert_equal IssueStatus.count, assigns(:statuses).size
80 80
81 81 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]'
82 82 end
83 83
84 84 def test_post_edit
85 85 WorkflowTransition.delete_all
86 86
87 87 post :edit, :role_id => 2, :tracker_id => 1,
88 88 :transitions => {
89 89 '4' => {'5' => {'always' => '1'}},
90 90 '3' => {'1' => {'always' => '1'}, '2' => {'always' => '1'}}
91 91 }
92 92 assert_response 302
93 93
94 94 assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
95 95 assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
96 96 assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first
97 97 end
98 98
99 99 def test_post_edit_with_additional_transitions
100 100 WorkflowTransition.delete_all
101 101
102 102 post :edit, :role_id => 2, :tracker_id => 1,
103 103 :transitions => {
104 104 '4' => {'5' => {'always' => '1', 'author' => '0', 'assignee' => '0'}},
105 105 '3' => {'1' => {'always' => '0', 'author' => '1', 'assignee' => '0'},
106 106 '2' => {'always' => '0', 'author' => '0', 'assignee' => '1'},
107 107 '4' => {'always' => '0', 'author' => '1', 'assignee' => '1'}}
108 108 }
109 109 assert_response 302
110 110
111 111 assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
112 112
113 113 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first
114 114 assert ! w.author
115 115 assert ! w.assignee
116 116 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first
117 117 assert w.author
118 118 assert ! w.assignee
119 119 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
120 120 assert ! w.author
121 121 assert w.assignee
122 122 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first
123 123 assert w.author
124 124 assert w.assignee
125 125 end
126 126
127 127 def test_get_permissions
128 128 get :permissions
129 129
130 130 assert_response :success
131 131 assert_template 'permissions'
132 132 end
133 133
134 134 def test_get_permissions_with_role_and_tracker
135 135 WorkflowPermission.delete_all
136 136 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
137 137 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
138 138 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
139 139
140 140 get :permissions, :role_id => 1, :tracker_id => 2
141 141 assert_response :success
142 142 assert_template 'permissions'
143 143
144 144 assert_select 'input[name=?][value="1"]', 'role_id[]'
145 145 assert_select 'input[name=?][value="2"]', 'tracker_id[]'
146 146
147 147 # Required field
148 148 assert_select 'select[name=?]', 'permissions[2][assigned_to_id]' do
149 149 assert_select 'option[value=""]'
150 150 assert_select 'option[value=""][selected=selected]', 0
151 151 assert_select 'option[value=readonly]', :text => 'Read-only'
152 152 assert_select 'option[value=readonly][selected=selected]', 0
153 153 assert_select 'option[value=required]', :text => 'Required'
154 154 assert_select 'option[value=required][selected=selected]'
155 155 end
156 156
157 157 # Read-only field
158 158 assert_select 'select[name=?]', 'permissions[3][fixed_version_id]' do
159 159 assert_select 'option[value=""]'
160 160 assert_select 'option[value=""][selected=selected]', 0
161 161 assert_select 'option[value=readonly]', :text => 'Read-only'
162 162 assert_select 'option[value=readonly][selected=selected]'
163 163 assert_select 'option[value=required]', :text => 'Required'
164 164 assert_select 'option[value=required][selected=selected]', 0
165 165 end
166 166
167 167 # Other field
168 168 assert_select 'select[name=?]', 'permissions[3][due_date]' do
169 169 assert_select 'option[value=""]'
170 170 assert_select 'option[value=""][selected=selected]', 0
171 171 assert_select 'option[value=readonly]', :text => 'Read-only'
172 172 assert_select 'option[value=readonly][selected=selected]', 0
173 173 assert_select 'option[value=required]', :text => 'Required'
174 174 assert_select 'option[value=required][selected=selected]', 0
175 175 end
176 176 end
177 177
178 178 def test_get_permissions_with_required_custom_field_should_not_show_required_option
179 179 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
180 180
181 181 get :permissions, :role_id => 1, :tracker_id => 1
182 182 assert_response :success
183 183 assert_template 'permissions'
184 184
185 185 # Custom field that is always required
186 186 # The default option is "(Required)"
187 187 assert_select 'select[name=?]', "permissions[3][#{cf.id}]" do
188 188 assert_select 'option[value=""]'
189 189 assert_select 'option[value=readonly]', :text => 'Read-only'
190 190 assert_select 'option[value=required]', 0
191 191 end
192 192 end
193 193
194 194 def test_get_permissions_should_disable_hidden_custom_fields
195 195 cf1 = IssueCustomField.generate!(:tracker_ids => [1], :visible => true)
196 196 cf2 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1])
197 197 cf3 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1, 2])
198 198
199 199 get :permissions, :role_id => 2, :tracker_id => 1
200 200 assert_response :success
201 201 assert_template 'permissions'
202 202
203 203 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf1.id}]"
204 204 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf3.id}]"
205 205
206 206 assert_select 'select[name=?][disabled=disabled]', "permissions[1][#{cf2.id}]" do
207 207 assert_select 'option[value=""][selected=selected]', :text => 'Hidden'
208 208 end
209 209 end
210 210
211 211 def test_get_permissions_with_missing_permissions_for_roles_should_default_to_no_change
212 212 WorkflowPermission.delete_all
213 213 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
214 214
215 215 get :permissions, :role_id => [1, 2], :tracker_id => 2
216 216 assert_response :success
217 217
218 218 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
219 219 assert_select 'option[selected]', 1
220 220 assert_select 'option[selected][value=no_change]'
221 221 end
222 222 end
223 223
224 224 def test_get_permissions_with_different_permissions_for_roles_should_default_to_no_change
225 225 WorkflowPermission.delete_all
226 226 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
227 227 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'readonly')
228 228
229 229 get :permissions, :role_id => [1, 2], :tracker_id => 2
230 230 assert_response :success
231 231
232 232 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
233 233 assert_select 'option[selected]', 1
234 234 assert_select 'option[selected][value=no_change]'
235 235 end
236 236 end
237 237
238 238 def test_get_permissions_with_same_permissions_for_roles_should_default_to_permission
239 239 WorkflowPermission.delete_all
240 240 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
241 241 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
242 242
243 243 get :permissions, :role_id => [1, 2], :tracker_id => 2
244 244 assert_response :success
245 245
246 246 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
247 247 assert_select 'option[selected]', 1
248 248 assert_select 'option[selected][value=required]'
249 249 end
250 250 end
251 251
252 252 def test_get_permissions_with_role_and_tracker_and_all_statuses_should_show_all_statuses
253 253 WorkflowTransition.delete_all
254 254
255 255 get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
256 256 assert_response :success
257 257 assert_equal IssueStatus.sorted.to_a, assigns(:statuses)
258 258 end
259 259
260 260 def test_post_permissions
261 261 WorkflowPermission.delete_all
262 262
263 263 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
264 264 '1' => {'assigned_to_id' => '', 'fixed_version_id' => 'required', 'due_date' => ''},
265 265 '2' => {'assigned_to_id' => 'readonly', 'fixed_version_id' => 'readonly', 'due_date' => ''},
266 266 '3' => {'assigned_to_id' => '', 'fixed_version_id' => '', 'due_date' => ''}
267 267 }
268 268 assert_response 302
269 269
270 270 workflows = WorkflowPermission.all
271 271 assert_equal 3, workflows.size
272 272 workflows.each do |workflow|
273 273 assert_equal 1, workflow.role_id
274 274 assert_equal 2, workflow.tracker_id
275 275 end
276 276 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
277 277 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
278 278 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
279 279 end
280 280
281 281 def test_get_copy
282 282 get :copy
283 283 assert_response :success
284 284 assert_template 'copy'
285 285 assert_select 'select[name=source_tracker_id]' do
286 286 assert_select 'option[value="1"]', :text => 'Bug'
287 287 end
288 288 assert_select 'select[name=source_role_id]' do
289 289 assert_select 'option[value="2"]', :text => 'Developer'
290 290 end
291 291 assert_select 'select[name=?]', 'target_tracker_ids[]' do
292 292 assert_select 'option[value="3"]', :text => 'Support request'
293 293 end
294 294 assert_select 'select[name=?]', 'target_role_ids[]' do
295 295 assert_select 'option[value="1"]', :text => 'Manager'
296 296 end
297 297 end
298 298
299 299 def test_post_copy_one_to_one
300 300 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
301 301
302 302 post :copy, :source_tracker_id => '1', :source_role_id => '2',
303 303 :target_tracker_ids => ['3'], :target_role_ids => ['1']
304 304 assert_response 302
305 305 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
306 306 end
307 307
308 308 def test_post_copy_one_to_many
309 309 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
310 310
311 311 post :copy, :source_tracker_id => '1', :source_role_id => '2',
312 312 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
313 313 assert_response 302
314 314 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
315 315 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
316 316 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
317 317 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
318 318 end
319 319
320 320 def test_post_copy_many_to_many
321 321 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
322 322 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
323 323
324 324 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
325 325 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
326 326 assert_response 302
327 327 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
328 328 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
329 329 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
330 330 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
331 331 end
332 332
333 333 def test_post_copy_with_incomplete_source_specification_should_fail
334 334 assert_no_difference 'WorkflowRule.count' do
335 335 post :copy,
336 336 :source_tracker_id => '', :source_role_id => '2',
337 337 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
338 338 assert_response 200
339 339 assert_select 'div.flash.error', :text => 'Please select a source tracker or role'
340 340 end
341 341 end
342 342
343 343 def test_post_copy_with_incomplete_target_specification_should_fail
344 344 assert_no_difference 'WorkflowRule.count' do
345 345 post :copy,
346 346 :source_tracker_id => '1', :source_role_id => '2',
347 347 :target_tracker_ids => ['2', '3']
348 348 assert_response 200
349 349 assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)'
350 350 end
351 351 end
352 352
353 353 # Returns an array of status transitions that can be compared
354 354 def status_transitions(conditions)
355 355 WorkflowTransition.
356 356 where(conditions).
357 357 order('tracker_id, role_id, old_status_id, new_status_id').
358 358 collect {|w| [w.old_status, w.new_status_id]}
359 359 end
360 360 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from test/ui/issues_test.rb to test/ui/issues_test_ui.rb
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now