##// END OF EJS Templates
Fixed that sidebar with hook content only should not be hidden....
Jean-Philippe Lang -
r9415:f54ecfc55f78
parent child
Show More
@@ -0,0 +1,67
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../test_helper', __FILE__)
19
20 class MenuManagerTest < ActionController::IntegrationTest
21
22 fixtures :users, :roles, :projects, :members, :member_roles
23
24 # Hooks that are manually registered later
25 class ProjectBasedTemplate < Redmine::Hook::ViewListener
26 def view_layouts_base_html_head(context)
27 # Adds a project stylesheet
28 stylesheet_link_tag(context[:project].identifier) if context[:project]
29 end
30 end
31
32 class SidebarContent < Redmine::Hook::ViewListener
33 def view_layouts_base_sidebar(context)
34 content_tag('p', 'Sidebar hook')
35 end
36 end
37
38 def setup
39 Redmine::Hook.clear_listeners
40 end
41
42 def teardown
43 Redmine::Hook.clear_listeners
44 end
45
46 def test_html_head_hook_response
47 Redmine::Hook.add_listener(ProjectBasedTemplate)
48
49 get '/projects/ecookbook'
50 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
51 :parent => {:tag => 'head'}
52 end
53
54 def test_empty_sidebar_should_be_hidden
55 get '/'
56 assert_select 'div#main.nosidebar'
57 end
58
59 def test_sidebar_with_hook_content_should_not_be_hidden
60 Redmine::Hook.add_listener(SidebarContent)
61
62 get '/'
63 assert_select 'div#sidebar p', :text => 'Sidebar hook'
64 assert_select 'div#main'
65 assert_select 'div#main.nosidebar', 0
66 end
67 end
@@ -1,1186 +1,1194
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require '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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 opt_only_path = {}
101 101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102 102 options.delete(:only_path)
103 103 link_to(h(text),
104 104 {:controller => 'attachments', :action => action,
105 105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106 106 options)
107 107 end
108 108
109 109 # Generates a link to a SCM revision
110 110 # Options:
111 111 # * :text - Link text (default to the formatted revision)
112 112 def link_to_revision(revision, repository, options={})
113 113 if repository.is_a?(Project)
114 114 repository = repository.repository
115 115 end
116 116 text = options.delete(:text) || format_revision(revision)
117 117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
118 118 link_to(
119 119 h(text),
120 120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
121 121 :title => l(:label_revision_id, format_revision(revision))
122 122 )
123 123 end
124 124
125 125 # Generates a link to a message
126 126 def link_to_message(message, options={}, html_options = nil)
127 127 link_to(
128 128 h(truncate(message.subject, :length => 60)),
129 129 { :controller => 'messages', :action => 'show',
130 130 :board_id => message.board_id,
131 131 :id => message.root,
132 132 :r => (message.parent_id && message.id),
133 133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
134 134 }.merge(options),
135 135 html_options
136 136 )
137 137 end
138 138
139 139 # Generates a link to a project if active
140 140 # Examples:
141 141 #
142 142 # link_to_project(project) # => link to the specified project overview
143 143 # link_to_project(project, :action=>'settings') # => link to project settings
144 144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
145 145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
146 146 #
147 147 def link_to_project(project, options={}, html_options = nil)
148 148 if project.active?
149 149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
150 150 link_to(h(project), url, html_options)
151 151 else
152 152 h(project)
153 153 end
154 154 end
155 155
156 156 def toggle_link(name, id, options={})
157 157 onclick = "Element.toggle('#{id}'); "
158 158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
159 159 onclick << "return false;"
160 160 link_to(name, "#", :onclick => onclick)
161 161 end
162 162
163 163 def image_to_function(name, function, html_options = {})
164 164 html_options.symbolize_keys!
165 165 tag(:input, html_options.merge({
166 166 :type => "image", :src => image_path(name),
167 167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 168 }))
169 169 end
170 170
171 171 def prompt_to_remote(name, text, param, url, html_options = {})
172 172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
173 173 link_to name, {}, html_options
174 174 end
175 175
176 176 def format_activity_title(text)
177 177 h(truncate_single_line(text, :length => 100))
178 178 end
179 179
180 180 def format_activity_day(date)
181 181 date == Date.today ? l(:label_today).titleize : format_date(date)
182 182 end
183 183
184 184 def format_activity_description(text)
185 185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 187 end
188 188
189 189 def format_version_name(version)
190 190 if version.project == @project
191 191 h(version)
192 192 else
193 193 h("#{version.project} - #{version}")
194 194 end
195 195 end
196 196
197 197 def due_date_distance_in_words(date)
198 198 if date
199 199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 200 end
201 201 end
202 202
203 203 def render_page_hierarchy(pages, node=nil, options={})
204 204 content = ''
205 205 if pages[node]
206 206 content << "<ul class=\"pages-hierarchy\">\n"
207 207 pages[node].each do |page|
208 208 content << "<li>"
209 209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
211 211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
212 212 content << "</li>\n"
213 213 end
214 214 content << "</ul>\n"
215 215 end
216 216 content.html_safe
217 217 end
218 218
219 219 # Renders flash messages
220 220 def render_flash_messages
221 221 s = ''
222 222 flash.each do |k,v|
223 223 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
224 224 end
225 225 s.html_safe
226 226 end
227 227
228 228 # Renders tabs and their content
229 229 def render_tabs(tabs)
230 230 if tabs.any?
231 231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
232 232 else
233 233 content_tag 'p', l(:label_no_data), :class => "nodata"
234 234 end
235 235 end
236 236
237 237 # Renders the project quick-jump box
238 238 def render_project_jump_box
239 239 return unless User.current.logged?
240 240 projects = User.current.memberships.collect(&:project).compact.uniq
241 241 if projects.any?
242 242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243 243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
244 244 '<option value="" disabled="disabled">---</option>'
245 245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
246 246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 247 end
248 248 s << '</select>'
249 249 s.html_safe
250 250 end
251 251 end
252 252
253 253 def project_tree_options_for_select(projects, options = {})
254 254 s = ''
255 255 project_tree(projects) do |project, level|
256 256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 257 tag_options = {:value => project.id}
258 258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
259 259 tag_options[:selected] = 'selected'
260 260 else
261 261 tag_options[:selected] = nil
262 262 end
263 263 tag_options.merge!(yield(project)) if block_given?
264 264 s << content_tag('option', name_prefix + h(project), tag_options)
265 265 end
266 266 s.html_safe
267 267 end
268 268
269 269 # Yields the given block for each project with its level in the tree
270 270 #
271 271 # Wrapper for Project#project_tree
272 272 def project_tree(projects, &block)
273 273 Project.project_tree(projects, &block)
274 274 end
275 275
276 276 def project_nested_ul(projects, &block)
277 277 s = ''
278 278 if projects.any?
279 279 ancestors = []
280 280 projects.sort_by(&:lft).each do |project|
281 281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
282 282 s << "<ul>\n"
283 283 else
284 284 ancestors.pop
285 285 s << "</li>"
286 286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 287 ancestors.pop
288 288 s << "</ul></li>\n"
289 289 end
290 290 end
291 291 s << "<li>"
292 292 s << yield(project).to_s
293 293 ancestors << project
294 294 end
295 295 s << ("</li></ul>\n" * ancestors.size)
296 296 end
297 297 s.html_safe
298 298 end
299 299
300 300 def principals_check_box_tags(name, principals)
301 301 s = ''
302 302 principals.sort.each do |principal|
303 303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
304 304 end
305 305 s.html_safe
306 306 end
307 307
308 308 # Returns a string for users/groups option tags
309 309 def principals_options_for_select(collection, selected=nil)
310 310 s = ''
311 311 if collection.include?(User.current)
312 312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313 313 end
314 314 groups = ''
315 315 collection.sort.each do |element|
316 316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
317 317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
318 318 end
319 319 unless groups.empty?
320 320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
321 321 end
322 322 s.html_safe
323 323 end
324 324
325 325 # Truncates and returns the string as a single line
326 326 def truncate_single_line(string, *args)
327 327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328 328 end
329 329
330 330 # Truncates at line break after 250 characters or options[:length]
331 331 def truncate_lines(string, options={})
332 332 length = options[:length] || 250
333 333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
334 334 "#{$1}..."
335 335 else
336 336 string
337 337 end
338 338 end
339 339
340 340 def anchor(text)
341 341 text.to_s.gsub(' ', '_')
342 342 end
343 343
344 344 def html_hours(text)
345 345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346 346 end
347 347
348 348 def authoring(created, author, options={})
349 349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350 350 end
351 351
352 352 def time_tag(time)
353 353 text = distance_of_time_in_words(Time.now, time)
354 354 if @project
355 355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
356 356 else
357 357 content_tag('acronym', text, :title => format_time(time))
358 358 end
359 359 end
360 360
361 361 def syntax_highlight_lines(name, content)
362 362 lines = []
363 363 syntax_highlight(name, content).each_line { |line| lines << line }
364 364 lines
365 365 end
366 366
367 367 def syntax_highlight(name, content)
368 368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369 369 end
370 370
371 371 def to_path_param(path)
372 372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
373 373 end
374 374
375 375 def pagination_links_full(paginator, count=nil, options={})
376 376 page_param = options.delete(:page_param) || :page
377 377 per_page_links = options.delete(:per_page_links)
378 378 url_param = params.dup
379 379
380 380 html = ''
381 381 if paginator.current.previous
382 382 # \xc2\xab(utf-8) = &#171;
383 383 html << link_to_content_update(
384 384 "\xc2\xab " + l(:label_previous),
385 385 url_param.merge(page_param => paginator.current.previous)) + ' '
386 386 end
387 387
388 388 html << (pagination_links_each(paginator, options) do |n|
389 389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
390 390 end || '')
391 391
392 392 if paginator.current.next
393 393 # \xc2\xbb(utf-8) = &#187;
394 394 html << ' ' + link_to_content_update(
395 395 (l(:label_next) + " \xc2\xbb"),
396 396 url_param.merge(page_param => paginator.current.next))
397 397 end
398 398
399 399 unless count.nil?
400 400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
401 401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
402 402 html << " | #{links}"
403 403 end
404 404 end
405 405
406 406 html.html_safe
407 407 end
408 408
409 409 def per_page_links(selected=nil)
410 410 links = Setting.per_page_options_array.collect do |n|
411 411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
412 412 end
413 413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
414 414 end
415 415
416 416 def reorder_links(name, url, method = :post)
417 417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
418 418 url.merge({"#{name}[move_to]" => 'highest'}),
419 419 :method => method, :title => l(:label_sort_highest)) +
420 420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
421 421 url.merge({"#{name}[move_to]" => 'higher'}),
422 422 :method => method, :title => l(:label_sort_higher)) +
423 423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
424 424 url.merge({"#{name}[move_to]" => 'lower'}),
425 425 :method => method, :title => l(:label_sort_lower)) +
426 426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
427 427 url.merge({"#{name}[move_to]" => 'lowest'}),
428 428 :method => method, :title => l(:label_sort_lowest))
429 429 end
430 430
431 431 def breadcrumb(*args)
432 432 elements = args.flatten
433 433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
434 434 end
435 435
436 436 def other_formats_links(&block)
437 437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
438 438 yield Redmine::Views::OtherFormatsBuilder.new(self)
439 439 concat('</p>'.html_safe)
440 440 end
441 441
442 442 def page_header_title
443 443 if @project.nil? || @project.new_record?
444 444 h(Setting.app_title)
445 445 else
446 446 b = []
447 447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
448 448 if ancestors.any?
449 449 root = ancestors.shift
450 450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
451 451 if ancestors.size > 2
452 452 b << "\xe2\x80\xa6"
453 453 ancestors = ancestors[-2, 2]
454 454 end
455 455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
456 456 end
457 457 b << h(@project)
458 458 b.join(" \xc2\xbb ").html_safe
459 459 end
460 460 end
461 461
462 462 def html_title(*args)
463 463 if args.empty?
464 464 title = @html_title || []
465 465 title << @project.name if @project
466 466 title << Setting.app_title unless Setting.app_title == title.last
467 467 title.select {|t| !t.blank? }.join(' - ')
468 468 else
469 469 @html_title ||= []
470 470 @html_title += args
471 471 end
472 472 end
473 473
474 474 # Returns the theme, controller name, and action as css classes for the
475 475 # HTML body.
476 476 def body_css_classes
477 477 css = []
478 478 if theme = Redmine::Themes.theme(Setting.ui_theme)
479 479 css << 'theme-' + theme.name
480 480 end
481 481
482 482 css << 'controller-' + controller_name
483 483 css << 'action-' + action_name
484 484 css.join(' ')
485 485 end
486 486
487 487 def accesskey(s)
488 488 Redmine::AccessKeys.key_for s
489 489 end
490 490
491 491 # Formats text according to system settings.
492 492 # 2 ways to call this method:
493 493 # * with a String: textilizable(text, options)
494 494 # * with an object and one of its attribute: textilizable(issue, :description, options)
495 495 def textilizable(*args)
496 496 options = args.last.is_a?(Hash) ? args.pop : {}
497 497 case args.size
498 498 when 1
499 499 obj = options[:object]
500 500 text = args.shift
501 501 when 2
502 502 obj = args.shift
503 503 attr = args.shift
504 504 text = obj.send(attr).to_s
505 505 else
506 506 raise ArgumentError, 'invalid arguments to textilizable'
507 507 end
508 508 return '' if text.blank?
509 509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
510 510 only_path = options.delete(:only_path) == false ? false : true
511 511
512 512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
513 513
514 514 @parsed_headings = []
515 515 @heading_anchors = {}
516 516 @current_section = 0 if options[:edit_section_links]
517 517
518 518 parse_sections(text, project, obj, attr, only_path, options)
519 519 text = parse_non_pre_blocks(text) do |text|
520 520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
521 521 send method_name, text, project, obj, attr, only_path, options
522 522 end
523 523 end
524 524 parse_headings(text, project, obj, attr, only_path, options)
525 525
526 526 if @parsed_headings.any?
527 527 replace_toc(text, @parsed_headings)
528 528 end
529 529
530 530 text.html_safe
531 531 end
532 532
533 533 def parse_non_pre_blocks(text)
534 534 s = StringScanner.new(text)
535 535 tags = []
536 536 parsed = ''
537 537 while !s.eos?
538 538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
539 539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
540 540 if tags.empty?
541 541 yield text
542 542 end
543 543 parsed << text
544 544 if tag
545 545 if closing
546 546 if tags.last == tag.downcase
547 547 tags.pop
548 548 end
549 549 else
550 550 tags << tag.downcase
551 551 end
552 552 parsed << full_tag
553 553 end
554 554 end
555 555 # Close any non closing tags
556 556 while tag = tags.pop
557 557 parsed << "</#{tag}>"
558 558 end
559 559 parsed
560 560 end
561 561
562 562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
563 563 # when using an image link, try to use an attachment, if possible
564 564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
565 565 attachments = options[:attachments] || obj.attachments
566 566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
567 567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
568 568 # search for the picture in attachments
569 569 if found = Attachment.latest_attach(attachments, filename)
570 570 image_url = url_for :only_path => only_path, :controller => 'attachments',
571 571 :action => 'download', :id => found
572 572 desc = found.description.to_s.gsub('"', '')
573 573 if !desc.blank? && alttext.blank?
574 574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
575 575 end
576 576 "src=\"#{image_url}\"#{alt}"
577 577 else
578 578 m
579 579 end
580 580 end
581 581 end
582 582 end
583 583
584 584 # Wiki links
585 585 #
586 586 # Examples:
587 587 # [[mypage]]
588 588 # [[mypage|mytext]]
589 589 # wiki links can refer other project wikis, using project name or identifier:
590 590 # [[project:]] -> wiki starting page
591 591 # [[project:|mytext]]
592 592 # [[project:mypage]]
593 593 # [[project:mypage|mytext]]
594 594 def parse_wiki_links(text, project, obj, attr, only_path, options)
595 595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
596 596 link_project = project
597 597 esc, all, page, title = $1, $2, $3, $5
598 598 if esc.nil?
599 599 if page =~ /^([^\:]+)\:(.*)$/
600 600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
601 601 page = $2
602 602 title ||= $1 if page.blank?
603 603 end
604 604
605 605 if link_project && link_project.wiki
606 606 # extract anchor
607 607 anchor = nil
608 608 if page =~ /^(.+?)\#(.+)$/
609 609 page, anchor = $1, $2
610 610 end
611 611 anchor = sanitize_anchor_name(anchor) if anchor.present?
612 612 # check if page exists
613 613 wiki_page = link_project.wiki.find_page(page)
614 614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
615 615 "##{anchor}"
616 616 else
617 617 case options[:wiki_links]
618 618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
619 619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
620 620 else
621 621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
622 622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
623 623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
624 624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
625 625 end
626 626 end
627 627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
628 628 else
629 629 # project or wiki doesn't exist
630 630 all
631 631 end
632 632 else
633 633 all
634 634 end
635 635 end
636 636 end
637 637
638 638 # Redmine links
639 639 #
640 640 # Examples:
641 641 # Issues:
642 642 # #52 -> Link to issue #52
643 643 # Changesets:
644 644 # r52 -> Link to revision 52
645 645 # commit:a85130f -> Link to scmid starting with a85130f
646 646 # Documents:
647 647 # document#17 -> Link to document with id 17
648 648 # document:Greetings -> Link to the document with title "Greetings"
649 649 # document:"Some document" -> Link to the document with title "Some document"
650 650 # Versions:
651 651 # version#3 -> Link to version with id 3
652 652 # version:1.0.0 -> Link to version named "1.0.0"
653 653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
654 654 # Attachments:
655 655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
656 656 # Source files:
657 657 # source:some/file -> Link to the file located at /some/file in the project's repository
658 658 # source:some/file@52 -> Link to the file's revision 52
659 659 # source:some/file#L120 -> Link to line 120 of the file
660 660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
661 661 # export:some/file -> Force the download of the file
662 662 # Forum messages:
663 663 # message#1218 -> Link to message with id 1218
664 664 #
665 665 # Links can refer other objects from other projects, using project identifier:
666 666 # identifier:r52
667 667 # identifier:document:"Some document"
668 668 # identifier:version:1.0.0
669 669 # identifier:source:some/file
670 670 def parse_redmine_links(text, project, obj, attr, only_path, options)
671 671 text.gsub!(%r{([\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|
672 672 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
673 673 link = nil
674 674 if project_identifier
675 675 project = Project.visible.find_by_identifier(project_identifier)
676 676 end
677 677 if esc.nil?
678 678 if prefix.nil? && sep == 'r'
679 679 if project
680 680 repository = nil
681 681 if repo_identifier
682 682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
683 683 else
684 684 repository = project.repository
685 685 end
686 686 # project.changesets.visible raises an SQL error because of a double join on repositories
687 687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
688 688 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
689 689 :class => 'changeset',
690 690 :title => truncate_single_line(changeset.comments, :length => 100))
691 691 end
692 692 end
693 693 elsif sep == '#'
694 694 oid = identifier.to_i
695 695 case prefix
696 696 when nil
697 697 if issue = Issue.visible.find_by_id(oid, :include => :status)
698 698 anchor = comment_id ? "note-#{comment_id}" : nil
699 699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
700 700 :class => issue.css_classes,
701 701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
702 702 end
703 703 when 'document'
704 704 if document = Document.visible.find_by_id(oid)
705 705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
706 706 :class => 'document'
707 707 end
708 708 when 'version'
709 709 if version = Version.visible.find_by_id(oid)
710 710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
711 711 :class => 'version'
712 712 end
713 713 when 'message'
714 714 if message = Message.visible.find_by_id(oid, :include => :parent)
715 715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
716 716 end
717 717 when 'forum'
718 718 if board = Board.visible.find_by_id(oid)
719 719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
720 720 :class => 'board'
721 721 end
722 722 when 'news'
723 723 if news = News.visible.find_by_id(oid)
724 724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
725 725 :class => 'news'
726 726 end
727 727 when 'project'
728 728 if p = Project.visible.find_by_id(oid)
729 729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
730 730 end
731 731 end
732 732 elsif sep == ':'
733 733 # removes the double quotes if any
734 734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
735 735 case prefix
736 736 when 'document'
737 737 if project && document = project.documents.visible.find_by_title(name)
738 738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
739 739 :class => 'document'
740 740 end
741 741 when 'version'
742 742 if project && version = project.versions.visible.find_by_name(name)
743 743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
744 744 :class => 'version'
745 745 end
746 746 when 'forum'
747 747 if project && board = project.boards.visible.find_by_name(name)
748 748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
749 749 :class => 'board'
750 750 end
751 751 when 'news'
752 752 if project && news = project.news.visible.find_by_title(name)
753 753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
754 754 :class => 'news'
755 755 end
756 756 when 'commit', 'source', 'export'
757 757 if project
758 758 repository = nil
759 759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
760 760 repo_prefix, repo_identifier, name = $1, $2, $3
761 761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
762 762 else
763 763 repository = project.repository
764 764 end
765 765 if prefix == 'commit'
766 766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
767 767 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},
768 768 :class => 'changeset',
769 769 :title => truncate_single_line(h(changeset.comments), :length => 100)
770 770 end
771 771 else
772 772 if repository && User.current.allowed_to?(:browse_repository, project)
773 773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
774 774 path, rev, anchor = $1, $3, $5
775 775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
776 776 :path => to_path_param(path),
777 777 :rev => rev,
778 778 :anchor => anchor,
779 779 :format => (prefix == 'export' ? 'raw' : nil)},
780 780 :class => (prefix == 'export' ? 'source download' : 'source')
781 781 end
782 782 end
783 783 repo_prefix = nil
784 784 end
785 785 when 'attachment'
786 786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
787 787 if attachments && attachment = attachments.detect {|a| a.filename == name }
788 788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
789 789 :class => 'attachment'
790 790 end
791 791 when 'project'
792 792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
793 793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
794 794 end
795 795 end
796 796 end
797 797 end
798 798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
799 799 end
800 800 end
801 801
802 802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
803 803
804 804 def parse_sections(text, project, obj, attr, only_path, options)
805 805 return unless options[:edit_section_links]
806 806 text.gsub!(HEADING_RE) do
807 807 heading = $1
808 808 @current_section += 1
809 809 if @current_section > 1
810 810 content_tag('div',
811 811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
812 812 :class => 'contextual',
813 813 :title => l(:button_edit_section)) + heading.html_safe
814 814 else
815 815 heading
816 816 end
817 817 end
818 818 end
819 819
820 820 # Headings and TOC
821 821 # Adds ids and links to headings unless options[:headings] is set to false
822 822 def parse_headings(text, project, obj, attr, only_path, options)
823 823 return if options[:headings] == false
824 824
825 825 text.gsub!(HEADING_RE) do
826 826 level, attrs, content = $2.to_i, $3, $4
827 827 item = strip_tags(content).strip
828 828 anchor = sanitize_anchor_name(item)
829 829 # used for single-file wiki export
830 830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
831 831 @heading_anchors[anchor] ||= 0
832 832 idx = (@heading_anchors[anchor] += 1)
833 833 if idx > 1
834 834 anchor = "#{anchor}-#{idx}"
835 835 end
836 836 @parsed_headings << [level, anchor, item]
837 837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
838 838 end
839 839 end
840 840
841 841 MACROS_RE = /
842 842 (!)? # escaping
843 843 (
844 844 \{\{ # opening tag
845 845 ([\w]+) # macro name
846 846 (\(([^\}]*)\))? # optional arguments
847 847 \}\} # closing tag
848 848 )
849 849 /x unless const_defined?(:MACROS_RE)
850 850
851 851 # Macros substitution
852 852 def parse_macros(text, project, obj, attr, only_path, options)
853 853 text.gsub!(MACROS_RE) do
854 854 esc, all, macro = $1, $2, $3.downcase
855 855 args = ($5 || '').split(',').each(&:strip)
856 856 if esc.nil?
857 857 begin
858 858 exec_macro(macro, obj, args)
859 859 rescue => e
860 860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
861 861 end || all
862 862 else
863 863 all
864 864 end
865 865 end
866 866 end
867 867
868 868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
869 869
870 870 # Renders the TOC with given headings
871 871 def replace_toc(text, headings)
872 872 text.gsub!(TOC_RE) do
873 873 if headings.empty?
874 874 ''
875 875 else
876 876 div_class = 'toc'
877 877 div_class << ' right' if $1 == '>'
878 878 div_class << ' left' if $1 == '<'
879 879 out = "<ul class=\"#{div_class}\"><li>"
880 880 root = headings.map(&:first).min
881 881 current = root
882 882 started = false
883 883 headings.each do |level, anchor, item|
884 884 if level > current
885 885 out << '<ul><li>' * (level - current)
886 886 elsif level < current
887 887 out << "</li></ul>\n" * (current - level) + "</li><li>"
888 888 elsif started
889 889 out << '</li><li>'
890 890 end
891 891 out << "<a href=\"##{anchor}\">#{item}</a>"
892 892 current = level
893 893 started = true
894 894 end
895 895 out << '</li></ul>' * (current - root)
896 896 out << '</li></ul>'
897 897 end
898 898 end
899 899 end
900 900
901 901 # Same as Rails' simple_format helper without using paragraphs
902 902 def simple_format_without_paragraph(text)
903 903 text.to_s.
904 904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
905 905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
906 906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
907 907 html_safe
908 908 end
909 909
910 910 def lang_options_for_select(blank=true)
911 911 (blank ? [["(auto)", ""]] : []) +
912 912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
913 913 end
914 914
915 915 def label_tag_for(name, option_tags = nil, options = {})
916 916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
917 917 content_tag("label", label_text)
918 918 end
919 919
920 920 def labelled_tabular_form_for(*args, &proc)
921 921 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
922 922 args << {} unless args.last.is_a?(Hash)
923 923 options = args.last
924 924 options[:html] ||= {}
925 925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
926 926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
927 927 form_for(*args, &proc)
928 928 end
929 929
930 930 def labelled_form_for(*args, &proc)
931 931 args << {} unless args.last.is_a?(Hash)
932 932 options = args.last
933 933 if args.first.is_a?(Symbol)
934 934 options.merge!(:as => args.shift)
935 935 end
936 936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
937 937 form_for(*args, &proc)
938 938 end
939 939
940 940 def labelled_fields_for(*args, &proc)
941 941 args << {} unless args.last.is_a?(Hash)
942 942 options = args.last
943 943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
944 944 fields_for(*args, &proc)
945 945 end
946 946
947 947 def labelled_remote_form_for(*args, &proc)
948 948 args << {} unless args.last.is_a?(Hash)
949 949 options = args.last
950 950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
951 951 form_for(*args, &proc)
952 952 end
953 953
954 954 def error_messages_for(*objects)
955 955 html = ""
956 956 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
957 957 errors = objects.map {|o| o.errors.full_messages}.flatten
958 958 if errors.any?
959 959 html << "<div id='errorExplanation'><ul>\n"
960 960 errors.each do |error|
961 961 html << "<li>#{h error}</li>\n"
962 962 end
963 963 html << "</ul></div>\n"
964 964 end
965 965 html.html_safe
966 966 end
967 967
968 968 def back_url_hidden_field_tag
969 969 back_url = params[:back_url] || request.env['HTTP_REFERER']
970 970 back_url = CGI.unescape(back_url.to_s)
971 971 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
972 972 end
973 973
974 974 def check_all_links(form_name)
975 975 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
976 976 " | ".html_safe +
977 977 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
978 978 end
979 979
980 980 def progress_bar(pcts, options={})
981 981 pcts = [pcts, pcts] unless pcts.is_a?(Array)
982 982 pcts = pcts.collect(&:round)
983 983 pcts[1] = pcts[1] - pcts[0]
984 984 pcts << (100 - pcts[1] - pcts[0])
985 985 width = options[:width] || '100px;'
986 986 legend = options[:legend] || ''
987 987 content_tag('table',
988 988 content_tag('tr',
989 989 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
990 990 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
991 991 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
992 992 ), :class => 'progress', :style => "width: #{width};").html_safe +
993 993 content_tag('p', legend, :class => 'pourcent').html_safe
994 994 end
995 995
996 996 def checked_image(checked=true)
997 997 if checked
998 998 image_tag 'toggle_check.png'
999 999 end
1000 1000 end
1001 1001
1002 1002 def context_menu(url)
1003 1003 unless @context_menu_included
1004 1004 content_for :header_tags do
1005 1005 javascript_include_tag('context_menu') +
1006 1006 stylesheet_link_tag('context_menu')
1007 1007 end
1008 1008 if l(:direction) == 'rtl'
1009 1009 content_for :header_tags do
1010 1010 stylesheet_link_tag('context_menu_rtl')
1011 1011 end
1012 1012 end
1013 1013 @context_menu_included = true
1014 1014 end
1015 1015 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1016 1016 end
1017 1017
1018 1018 def calendar_for(field_id)
1019 1019 include_calendar_headers_tags
1020 1020 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1021 1021 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1022 1022 end
1023 1023
1024 1024 def include_calendar_headers_tags
1025 1025 unless @calendar_headers_tags_included
1026 1026 @calendar_headers_tags_included = true
1027 1027 content_for :header_tags do
1028 1028 start_of_week = case Setting.start_of_week.to_i
1029 1029 when 1
1030 1030 'Calendar._FD = 1;' # Monday
1031 1031 when 7
1032 1032 'Calendar._FD = 0;' # Sunday
1033 1033 when 6
1034 1034 'Calendar._FD = 6;' # Saturday
1035 1035 else
1036 1036 '' # use language
1037 1037 end
1038 1038
1039 1039 javascript_include_tag('calendar/calendar') +
1040 1040 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1041 1041 javascript_tag(start_of_week) +
1042 1042 javascript_include_tag('calendar/calendar-setup') +
1043 1043 stylesheet_link_tag('calendar')
1044 1044 end
1045 1045 end
1046 1046 end
1047 1047
1048 1048 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1049 1049 # Examples:
1050 1050 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1051 1051 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1052 1052 #
1053 1053 def stylesheet_link_tag(*sources)
1054 1054 options = sources.last.is_a?(Hash) ? sources.pop : {}
1055 1055 plugin = options.delete(:plugin)
1056 1056 sources = sources.map do |source|
1057 1057 if plugin
1058 1058 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1059 1059 elsif current_theme && current_theme.stylesheets.include?(source)
1060 1060 current_theme.stylesheet_path(source)
1061 1061 else
1062 1062 source
1063 1063 end
1064 1064 end
1065 1065 super sources, options
1066 1066 end
1067 1067
1068 1068 # Overrides Rails' image_tag with themes and plugins support.
1069 1069 # Examples:
1070 1070 # image_tag('image.png') # => picks image.png from the current theme or defaults
1071 1071 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1072 1072 #
1073 1073 def image_tag(source, options={})
1074 1074 if plugin = options.delete(:plugin)
1075 1075 source = "/plugin_assets/#{plugin}/images/#{source}"
1076 1076 elsif current_theme && current_theme.images.include?(source)
1077 1077 source = current_theme.image_path(source)
1078 1078 end
1079 1079 super source, options
1080 1080 end
1081 1081
1082 1082 # Overrides Rails' javascript_include_tag with plugins support
1083 1083 # Examples:
1084 1084 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1085 1085 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1086 1086 #
1087 1087 def javascript_include_tag(*sources)
1088 1088 options = sources.last.is_a?(Hash) ? sources.pop : {}
1089 1089 if plugin = options.delete(:plugin)
1090 1090 sources = sources.map do |source|
1091 1091 if plugin
1092 1092 "/plugin_assets/#{plugin}/javascripts/#{source}"
1093 1093 else
1094 1094 source
1095 1095 end
1096 1096 end
1097 1097 end
1098 1098 super sources, options
1099 1099 end
1100 1100
1101 1101 def content_for(name, content = nil, &block)
1102 1102 @has_content ||= {}
1103 1103 @has_content[name] = true
1104 1104 super(name, content, &block)
1105 1105 end
1106 1106
1107 1107 def has_content?(name)
1108 1108 (@has_content && @has_content[name]) || false
1109 1109 end
1110 1110
1111 def sidebar_content?
1112 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1113 end
1114
1115 def view_layouts_base_sidebar_hook_response
1116 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1117 end
1118
1111 1119 def email_delivery_enabled?
1112 1120 !!ActionMailer::Base.perform_deliveries
1113 1121 end
1114 1122
1115 1123 # Returns the avatar image tag for the given +user+ if avatars are enabled
1116 1124 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1117 1125 def avatar(user, options = { })
1118 1126 if Setting.gravatar_enabled?
1119 1127 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1120 1128 email = nil
1121 1129 if user.respond_to?(:mail)
1122 1130 email = user.mail
1123 1131 elsif user.to_s =~ %r{<(.+?)>}
1124 1132 email = $1
1125 1133 end
1126 1134 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1127 1135 else
1128 1136 ''
1129 1137 end
1130 1138 end
1131 1139
1132 1140 def sanitize_anchor_name(anchor)
1133 1141 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1134 1142 end
1135 1143
1136 1144 # Returns the javascript tags that are included in the html layout head
1137 1145 def javascript_heads
1138 1146 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1139 1147 unless User.current.pref.warn_on_leaving_unsaved == '0'
1140 1148 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1141 1149 end
1142 1150 tags
1143 1151 end
1144 1152
1145 1153 def favicon
1146 1154 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1147 1155 end
1148 1156
1149 1157 def robot_exclusion_tag
1150 1158 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1151 1159 end
1152 1160
1153 1161 # Returns true if arg is expected in the API response
1154 1162 def include_in_api_response?(arg)
1155 1163 unless @included_in_api_response
1156 1164 param = params[:include]
1157 1165 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1158 1166 @included_in_api_response.collect!(&:strip)
1159 1167 end
1160 1168 @included_in_api_response.include?(arg.to_s)
1161 1169 end
1162 1170
1163 1171 # Returns options or nil if nometa param or X-Redmine-Nometa header
1164 1172 # was set in the request
1165 1173 def api_meta(options)
1166 1174 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1167 1175 # compatibility mode for activeresource clients that raise
1168 1176 # an error when unserializing an array with attributes
1169 1177 nil
1170 1178 else
1171 1179 options
1172 1180 end
1173 1181 end
1174 1182
1175 1183 private
1176 1184
1177 1185 def wiki_helper
1178 1186 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1179 1187 extend helper
1180 1188 return self
1181 1189 end
1182 1190
1183 1191 def link_to_content_update(text, url_params = {}, html_options = {})
1184 1192 link_to(text, url_params, html_options)
1185 1193 end
1186 1194 end
@@ -1,85 +1,85
1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 3 <head>
4 4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 5 <title><%=h html_title %></title>
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
9 9 <%= csrf_meta_tag %>
10 10 <%= favicon %>
11 11 <%= stylesheet_link_tag 'application', :media => 'all' %>
12 12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 13 <%= javascript_heads %>
14 14 <%= heads_for_theme %>
15 15 <!--[if IE 6]>
16 16 <style type="text/css">
17 17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
18 18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
19 19 </style>
20 20 <![endif]-->
21 21 <%= call_hook :view_layouts_base_html_head %>
22 22 <!-- page specific tags -->
23 23 <%= yield :header_tags -%>
24 24 </head>
25 25 <body class="<%=h body_css_classes %>">
26 26 <div id="wrapper">
27 27 <div id="wrapper2">
28 28 <div id="top-menu">
29 29 <div id="account">
30 30 <%= render_menu :account_menu -%>
31 31 </div>
32 32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
33 33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
34 34 </div>
35 35
36 36 <div id="header">
37 37 <% if User.current.logged? || !Setting.login_required? %>
38 38 <div id="quick-search">
39 39 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
40 40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
41 41 <label for='q'>
42 42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
43 43 </label>
44 44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
45 45 <% end %>
46 46 <%= render_project_jump_box %>
47 47 </div>
48 48 <% end %>
49 49
50 50 <h1><%= page_header_title %></h1>
51 51
52 52 <% if display_main_menu?(@project) %>
53 53 <div id="main-menu">
54 54 <%= render_main_menu(@project) %>
55 55 </div>
56 56 <% end %>
57 57 </div>
58 58
59 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
59 <%= tag('div', {:id => 'main', :class => (sidebar_content? ? '' : 'nosidebar')}, true) %>
60 60 <div id="sidebar">
61 61 <%= yield :sidebar %>
62 <%= call_hook :view_layouts_base_sidebar %>
62 <%= view_layouts_base_sidebar_hook_response %>
63 63 </div>
64 64
65 65 <div id="content">
66 66 <%= render_flash_messages %>
67 67 <%= yield %>
68 68 <%= call_hook :view_layouts_base_content %>
69 69 <div style="clear:both;"></div>
70 70 </div>
71 71 </div>
72 72
73 73 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
74 74 <div id="ajax-modal" style="display:none;"></div>
75 75
76 76 <div id="footer">
77 77 <div class="bgl"><div class="bgr">
78 78 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
79 79 </div></div>
80 80 </div>
81 81 </div>
82 82 </div>
83 83 <%= call_hook :view_layouts_base_body_bottom %>
84 84 </body>
85 85 </html>
@@ -1,542 +1,523
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < ActionController::TestCase
25 25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 27 :attachments, :custom_fields, :custom_values, :time_entries
28 28
29 29 def setup
30 30 @controller = ProjectsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 @request.session[:user_id] = nil
34 34 Setting.default_language = 'en'
35 35 end
36 36
37 37 def test_index
38 38 get :index
39 39 assert_response :success
40 40 assert_template 'index'
41 41 assert_not_nil assigns(:projects)
42 42
43 43 assert_tag :ul, :child => {:tag => 'li',
44 44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 45 :child => { :tag => 'ul',
46 46 :descendant => { :tag => 'a',
47 47 :content => 'Child of private child'
48 48 }
49 49 }
50 50 }
51 51
52 52 assert_no_tag :a, :content => /Private child of eCookbook/
53 53 end
54 54
55 55 def test_index_atom
56 56 get :index, :format => 'atom'
57 57 assert_response :success
58 58 assert_template 'common/feed'
59 59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current))
61 61 end
62 62
63 63 context "#index" do
64 64 context "by non-admin user with view_time_entries permission" do
65 65 setup do
66 66 @request.session[:user_id] = 3
67 67 end
68 68 should "show overall spent time link" do
69 69 get :index
70 70 assert_template 'index'
71 71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 72 end
73 73 end
74 74
75 75 context "by non-admin user without view_time_entries permission" do
76 76 setup do
77 77 Role.find(2).remove_permission! :view_time_entries
78 78 Role.non_member.remove_permission! :view_time_entries
79 79 Role.anonymous.remove_permission! :view_time_entries
80 80 @request.session[:user_id] = 3
81 81 end
82 82 should "not show overall spent time link" do
83 83 get :index
84 84 assert_template 'index'
85 85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 86 end
87 87 end
88 88 end
89 89
90 90 context "#new" do
91 91 context "by admin user" do
92 92 setup do
93 93 @request.session[:user_id] = 1
94 94 end
95 95
96 96 should "accept get" do
97 97 get :new
98 98 assert_response :success
99 99 assert_template 'new'
100 100 end
101 101
102 102 end
103 103
104 104 context "by non-admin user with add_project permission" do
105 105 setup do
106 106 Role.non_member.add_permission! :add_project
107 107 @request.session[:user_id] = 9
108 108 end
109 109
110 110 should "accept get" do
111 111 get :new
112 112 assert_response :success
113 113 assert_template 'new'
114 114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 115 end
116 116 end
117 117
118 118 context "by non-admin user with add_subprojects permission" do
119 119 setup do
120 120 Role.find(1).remove_permission! :add_project
121 121 Role.find(1).add_permission! :add_subprojects
122 122 @request.session[:user_id] = 2
123 123 end
124 124
125 125 should "accept get" do
126 126 get :new, :parent_id => 'ecookbook'
127 127 assert_response :success
128 128 assert_template 'new'
129 129 # parent project selected
130 130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 132 # no empty value
133 133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 135 end
136 136 end
137 137
138 138 end
139 139
140 140 context "POST :create" do
141 141 context "by admin user" do
142 142 setup do
143 143 @request.session[:user_id] = 1
144 144 end
145 145
146 146 should "create a new project" do
147 147 post :create,
148 148 :project => {
149 149 :name => "blog",
150 150 :description => "weblog",
151 151 :homepage => 'http://weblog',
152 152 :identifier => "blog",
153 153 :is_public => 1,
154 154 :custom_field_values => { '3' => 'Beta' },
155 155 :tracker_ids => ['1', '3'],
156 156 # an issue custom field that is not for all project
157 157 :issue_custom_field_ids => ['9'],
158 158 :enabled_module_names => ['issue_tracking', 'news', 'repository']
159 159 }
160 160 assert_redirected_to '/projects/blog/settings'
161 161
162 162 project = Project.find_by_name('blog')
163 163 assert_kind_of Project, project
164 164 assert project.active?
165 165 assert_equal 'weblog', project.description
166 166 assert_equal 'http://weblog', project.homepage
167 167 assert_equal true, project.is_public?
168 168 assert_nil project.parent
169 169 assert_equal 'Beta', project.custom_value_for(3).value
170 170 assert_equal [1, 3], project.trackers.map(&:id).sort
171 171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
172 172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
173 173 end
174 174
175 175 should "create a new subproject" do
176 176 post :create, :project => { :name => "blog",
177 177 :description => "weblog",
178 178 :identifier => "blog",
179 179 :is_public => 1,
180 180 :custom_field_values => { '3' => 'Beta' },
181 181 :parent_id => 1
182 182 }
183 183 assert_redirected_to '/projects/blog/settings'
184 184
185 185 project = Project.find_by_name('blog')
186 186 assert_kind_of Project, project
187 187 assert_equal Project.find(1), project.parent
188 188 end
189 189
190 190 should "continue" do
191 191 assert_difference 'Project.count' do
192 192 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
193 193 end
194 194 assert_redirected_to '/projects/new?'
195 195 end
196 196 end
197 197
198 198 context "by non-admin user with add_project permission" do
199 199 setup do
200 200 Role.non_member.add_permission! :add_project
201 201 @request.session[:user_id] = 9
202 202 end
203 203
204 204 should "accept create a Project" do
205 205 post :create, :project => { :name => "blog",
206 206 :description => "weblog",
207 207 :identifier => "blog",
208 208 :is_public => 1,
209 209 :custom_field_values => { '3' => 'Beta' },
210 210 :tracker_ids => ['1', '3'],
211 211 :enabled_module_names => ['issue_tracking', 'news', 'repository']
212 212 }
213 213
214 214 assert_redirected_to '/projects/blog/settings'
215 215
216 216 project = Project.find_by_name('blog')
217 217 assert_kind_of Project, project
218 218 assert_equal 'weblog', project.description
219 219 assert_equal true, project.is_public?
220 220 assert_equal [1, 3], project.trackers.map(&:id).sort
221 221 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
222 222
223 223 # User should be added as a project member
224 224 assert User.find(9).member_of?(project)
225 225 assert_equal 1, project.members.size
226 226 end
227 227
228 228 should "fail with parent_id" do
229 229 assert_no_difference 'Project.count' do
230 230 post :create, :project => { :name => "blog",
231 231 :description => "weblog",
232 232 :identifier => "blog",
233 233 :is_public => 1,
234 234 :custom_field_values => { '3' => 'Beta' },
235 235 :parent_id => 1
236 236 }
237 237 end
238 238 assert_response :success
239 239 project = assigns(:project)
240 240 assert_kind_of Project, project
241 241 assert_not_nil project.errors[:parent_id]
242 242 end
243 243 end
244 244
245 245 context "by non-admin user with add_subprojects permission" do
246 246 setup do
247 247 Role.find(1).remove_permission! :add_project
248 248 Role.find(1).add_permission! :add_subprojects
249 249 @request.session[:user_id] = 2
250 250 end
251 251
252 252 should "create a project with a parent_id" do
253 253 post :create, :project => { :name => "blog",
254 254 :description => "weblog",
255 255 :identifier => "blog",
256 256 :is_public => 1,
257 257 :custom_field_values => { '3' => 'Beta' },
258 258 :parent_id => 1
259 259 }
260 260 assert_redirected_to '/projects/blog/settings'
261 261 project = Project.find_by_name('blog')
262 262 end
263 263
264 264 should "fail without parent_id" do
265 265 assert_no_difference 'Project.count' do
266 266 post :create, :project => { :name => "blog",
267 267 :description => "weblog",
268 268 :identifier => "blog",
269 269 :is_public => 1,
270 270 :custom_field_values => { '3' => 'Beta' }
271 271 }
272 272 end
273 273 assert_response :success
274 274 project = assigns(:project)
275 275 assert_kind_of Project, project
276 276 assert_not_nil project.errors[:parent_id]
277 277 end
278 278
279 279 should "fail with unauthorized parent_id" do
280 280 assert !User.find(2).member_of?(Project.find(6))
281 281 assert_no_difference 'Project.count' do
282 282 post :create, :project => { :name => "blog",
283 283 :description => "weblog",
284 284 :identifier => "blog",
285 285 :is_public => 1,
286 286 :custom_field_values => { '3' => 'Beta' },
287 287 :parent_id => 6
288 288 }
289 289 end
290 290 assert_response :success
291 291 project = assigns(:project)
292 292 assert_kind_of Project, project
293 293 assert_not_nil project.errors[:parent_id]
294 294 end
295 295 end
296 296 end
297 297
298 298 def test_create_should_preserve_modules_on_validation_failure
299 299 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
300 300 @request.session[:user_id] = 1
301 301 assert_no_difference 'Project.count' do
302 302 post :create, :project => {
303 303 :name => "blog",
304 304 :identifier => "",
305 305 :enabled_module_names => %w(issue_tracking news)
306 306 }
307 307 end
308 308 assert_response :success
309 309 project = assigns(:project)
310 310 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
311 311 end
312 312 end
313 313
314 314 def test_show_by_id
315 315 get :show, :id => 1
316 316 assert_response :success
317 317 assert_template 'show'
318 318 assert_not_nil assigns(:project)
319 319 end
320 320
321 321 def test_show_by_identifier
322 322 get :show, :id => 'ecookbook'
323 323 assert_response :success
324 324 assert_template 'show'
325 325 assert_not_nil assigns(:project)
326 326 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
327 327
328 328 assert_tag 'li', :content => /Development status/
329 329 end
330 330
331 331 def test_show_should_not_display_hidden_custom_fields
332 332 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
333 333 get :show, :id => 'ecookbook'
334 334 assert_response :success
335 335 assert_template 'show'
336 336 assert_not_nil assigns(:project)
337 337
338 338 assert_no_tag 'li', :content => /Development status/
339 339 end
340 340
341 341 def test_show_should_not_fail_when_custom_values_are_nil
342 342 project = Project.find_by_identifier('ecookbook')
343 343 project.custom_values.first.update_attribute(:value, nil)
344 344 get :show, :id => 'ecookbook'
345 345 assert_response :success
346 346 assert_template 'show'
347 347 assert_not_nil assigns(:project)
348 348 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
349 349 end
350 350
351 351 def show_archived_project_should_be_denied
352 352 project = Project.find_by_identifier('ecookbook')
353 353 project.archive!
354 354
355 355 get :show, :id => 'ecookbook'
356 356 assert_response 403
357 357 assert_nil assigns(:project)
358 358 assert_tag :tag => 'p', :content => /archived/
359 359 end
360 360
361 361 def test_private_subprojects_hidden
362 362 get :show, :id => 'ecookbook'
363 363 assert_response :success
364 364 assert_template 'show'
365 365 assert_no_tag :tag => 'a', :content => /Private child/
366 366 end
367 367
368 368 def test_private_subprojects_visible
369 369 @request.session[:user_id] = 2 # manager who is a member of the private subproject
370 370 get :show, :id => 'ecookbook'
371 371 assert_response :success
372 372 assert_template 'show'
373 373 assert_tag :tag => 'a', :content => /Private child/
374 374 end
375 375
376 376 def test_settings
377 377 @request.session[:user_id] = 2 # manager
378 378 get :settings, :id => 1
379 379 assert_response :success
380 380 assert_template 'settings'
381 381 end
382 382
383 383 def test_update
384 384 @request.session[:user_id] = 2 # manager
385 385 post :update, :id => 1, :project => {:name => 'Test changed name',
386 386 :issue_custom_field_ids => ['']}
387 387 assert_redirected_to '/projects/ecookbook/settings'
388 388 project = Project.find(1)
389 389 assert_equal 'Test changed name', project.name
390 390 end
391 391
392 392 def test_update_with_failure
393 393 @request.session[:user_id] = 2 # manager
394 394 post :update, :id => 1, :project => {:name => ''}
395 395 assert_response :success
396 396 assert_template 'settings'
397 397 assert_error_tag :content => /name can't be blank/i
398 398 end
399 399
400 400 def test_modules
401 401 @request.session[:user_id] = 2
402 402 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
403 403
404 404 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
405 405 assert_redirected_to '/projects/ecookbook/settings/modules'
406 406 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
407 407 end
408 408
409 409 def test_destroy_without_confirmation
410 410 @request.session[:user_id] = 1 # admin
411 411 delete :destroy, :id => 1
412 412 assert_response :success
413 413 assert_template 'destroy'
414 414 assert_not_nil Project.find_by_id(1)
415 415 end
416 416
417 417 def test_destroy
418 418 @request.session[:user_id] = 1 # admin
419 419 delete :destroy, :id => 1, :confirm => 1
420 420 assert_redirected_to '/admin/projects'
421 421 assert_nil Project.find_by_id(1)
422 422 end
423 423
424 424 def test_archive
425 425 @request.session[:user_id] = 1 # admin
426 426 post :archive, :id => 1
427 427 assert_redirected_to '/admin/projects'
428 428 assert !Project.find(1).active?
429 429 end
430 430
431 431 def test_archive_with_failure
432 432 @request.session[:user_id] = 1
433 433 Project.any_instance.stubs(:archive).returns(false)
434 434 post :archive, :id => 1
435 435 assert_redirected_to '/admin/projects'
436 436 assert_match /project cannot be archived/i, flash[:error]
437 437 end
438 438
439 439 def test_unarchive
440 440 @request.session[:user_id] = 1 # admin
441 441 Project.find(1).archive
442 442 post :unarchive, :id => 1
443 443 assert_redirected_to '/admin/projects'
444 444 assert Project.find(1).active?
445 445 end
446 446
447 447 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
448 448 CustomField.delete_all
449 449 parent = nil
450 450 6.times do |i|
451 451 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
452 452 p.set_parent!(parent)
453 453 get :show, :id => p
454 454 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
455 455 :children => { :count => [i, 3].min,
456 456 :only => { :tag => 'a' } }
457 457
458 458 parent = p
459 459 end
460 460 end
461 461
462 462 def test_get_copy
463 463 @request.session[:user_id] = 1 # admin
464 464 get :copy, :id => 1
465 465 assert_response :success
466 466 assert_template 'copy'
467 467 assert assigns(:project)
468 468 assert_equal Project.find(1).description, assigns(:project).description
469 469 assert_nil assigns(:project).id
470 470
471 471 assert_tag :tag => 'input',
472 472 :attributes => {:name => 'project[enabled_module_names][]', :value => 'issue_tracking'}
473 473 end
474 474
475 475 def test_post_copy_should_copy_requested_items
476 476 @request.session[:user_id] = 1 # admin
477 477 CustomField.delete_all
478 478
479 479 assert_difference 'Project.count' do
480 480 post :copy, :id => 1,
481 481 :project => {
482 482 :name => 'Copy',
483 483 :identifier => 'unique-copy',
484 484 :tracker_ids => ['1', '2', '3', ''],
485 485 :enabled_module_names => %w(issue_tracking time_tracking)
486 486 },
487 487 :only => %w(issues versions)
488 488 end
489 489 project = Project.find('unique-copy')
490 490 source = Project.find(1)
491 491 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
492 492
493 493 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
494 494 # issues assigned to a closed version won't be copied
495 495 assert_equal source.issues.select {|i| i.fixed_version.nil? || i.fixed_version.open?}.size,
496 496 project.issues.count, "All issues were not copied"
497 497 assert_equal 0, project.members.count
498 498 end
499 499
500 500 def test_post_copy_should_redirect_to_settings_when_successful
501 501 @request.session[:user_id] = 1 # admin
502 502 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
503 503 assert_response :redirect
504 504 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
505 505 end
506 506
507 507 def test_jump_should_redirect_to_active_tab
508 508 get :show, :id => 1, :jump => 'issues'
509 509 assert_redirected_to '/projects/ecookbook/issues'
510 510 end
511 511
512 512 def test_jump_should_not_redirect_to_inactive_tab
513 513 get :show, :id => 3, :jump => 'documents'
514 514 assert_response :success
515 515 assert_template 'show'
516 516 end
517 517
518 518 def test_jump_should_not_redirect_to_unknown_tab
519 519 get :show, :id => 3, :jump => 'foobar'
520 520 assert_response :success
521 521 assert_template 'show'
522 522 end
523
524 # A hook that is manually registered later
525 class ProjectBasedTemplate < Redmine::Hook::ViewListener
526 def view_layouts_base_html_head(context)
527 # Adds a project stylesheet
528 stylesheet_link_tag(context[:project].identifier) if context[:project]
529 end
530 end
531 # Don't use this hook now
532 Redmine::Hook.clear_listeners
533
534 def test_hook_response
535 Redmine::Hook.add_listener(ProjectBasedTemplate)
536 get :show, :id => 1
537 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
538 :parent => {:tag => 'head'}
539
540 Redmine::Hook.clear_listeners
541 end
542 523 end
General Comments 0
You need to be logged in to leave comments. Login now