##// END OF EJS Templates
Adds p/n access keys for previous/next links (#18692)....
Jean-Philippe Lang -
r13412:bfd811304601
parent child
Show More
@@ -1,1311 +1,1312
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 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 83 s = link_to(text, issue_path(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 96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 97 html_options = options.slice!(:only_path)
98 98 url = send(route_method, attachment, attachment.filename, options)
99 99 link_to text, url, html_options
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, repository, options={})
106 106 if repository.is_a?(Project)
107 107 repository = repository.repository
108 108 end
109 109 text = options.delete(:text) || format_revision(revision)
110 110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 111 link_to(
112 112 h(text),
113 113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision))
114 :title => l(:label_revision_id, format_revision(revision)),
115 :accesskey => options[:accesskey]
115 116 )
116 117 end
117 118
118 119 # Generates a link to a message
119 120 def link_to_message(message, options={}, html_options = nil)
120 121 link_to(
121 122 message.subject.truncate(60),
122 123 board_message_path(message.board_id, message.parent_id || message.id, {
123 124 :r => (message.parent_id && message.id),
124 125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 126 }.merge(options)),
126 127 html_options
127 128 )
128 129 end
129 130
130 131 # Generates a link to a project if active
131 132 # Examples:
132 133 #
133 134 # link_to_project(project) # => link to the specified project overview
134 135 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 136 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 137 #
137 138 def link_to_project(project, options={}, html_options = nil)
138 139 if project.archived?
139 140 h(project.name)
140 141 else
141 142 link_to project.name, project_path(project, options), html_options
142 143 end
143 144 end
144 145
145 146 # Generates a link to a project settings if active
146 147 def link_to_project_settings(project, options={}, html_options=nil)
147 148 if project.active?
148 149 link_to project.name, settings_project_path(project, options), html_options
149 150 elsif project.archived?
150 151 h(project.name)
151 152 else
152 153 link_to project.name, project_path(project, options), html_options
153 154 end
154 155 end
155 156
156 157 # Generates a link to a version
157 158 def link_to_version(version, options = {})
158 159 return '' unless version && version.is_a?(Version)
159 160 options = {:title => format_date(version.effective_date)}.merge(options)
160 161 link_to_if version.visible?, format_version_name(version), version_path(version), options
161 162 end
162 163
163 164 # Helper that formats object for html or text rendering
164 165 def format_object(object, html=true, &block)
165 166 if block_given?
166 167 object = yield object
167 168 end
168 169 case object.class.name
169 170 when 'Array'
170 171 object.map {|o| format_object(o, html)}.join(', ').html_safe
171 172 when 'Time'
172 173 format_time(object)
173 174 when 'Date'
174 175 format_date(object)
175 176 when 'Fixnum'
176 177 object.to_s
177 178 when 'Float'
178 179 sprintf "%.2f", object
179 180 when 'User'
180 181 html ? link_to_user(object) : object.to_s
181 182 when 'Project'
182 183 html ? link_to_project(object) : object.to_s
183 184 when 'Version'
184 185 html ? link_to_version(object) : object.to_s
185 186 when 'TrueClass'
186 187 l(:general_text_Yes)
187 188 when 'FalseClass'
188 189 l(:general_text_No)
189 190 when 'Issue'
190 191 object.visible? && html ? link_to_issue(object) : "##{object.id}"
191 192 when 'CustomValue', 'CustomFieldValue'
192 193 if object.custom_field
193 194 f = object.custom_field.format.formatted_custom_value(self, object, html)
194 195 if f.nil? || f.is_a?(String)
195 196 f
196 197 else
197 198 format_object(f, html, &block)
198 199 end
199 200 else
200 201 object.value.to_s
201 202 end
202 203 else
203 204 html ? h(object) : object.to_s
204 205 end
205 206 end
206 207
207 208 def wiki_page_path(page, options={})
208 209 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
209 210 end
210 211
211 212 def thumbnail_tag(attachment)
212 213 link_to image_tag(thumbnail_path(attachment)),
213 214 named_attachment_path(attachment, attachment.filename),
214 215 :title => attachment.filename
215 216 end
216 217
217 218 def toggle_link(name, id, options={})
218 219 onclick = "$('##{id}').toggle(); "
219 220 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
220 221 onclick << "return false;"
221 222 link_to(name, "#", :onclick => onclick)
222 223 end
223 224
224 225 def format_activity_title(text)
225 226 h(truncate_single_line_raw(text, 100))
226 227 end
227 228
228 229 def format_activity_day(date)
229 230 date == User.current.today ? l(:label_today).titleize : format_date(date)
230 231 end
231 232
232 233 def format_activity_description(text)
233 234 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
234 235 ).gsub(/[\r\n]+/, "<br />").html_safe
235 236 end
236 237
237 238 def format_version_name(version)
238 239 if !version.shared? || version.project == @project
239 240 h(version)
240 241 else
241 242 h("#{version.project} - #{version}")
242 243 end
243 244 end
244 245
245 246 def due_date_distance_in_words(date)
246 247 if date
247 248 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
248 249 end
249 250 end
250 251
251 252 # Renders a tree of projects as a nested set of unordered lists
252 253 # The given collection may be a subset of the whole project tree
253 254 # (eg. some intermediate nodes are private and can not be seen)
254 255 def render_project_nested_lists(projects, &block)
255 256 s = ''
256 257 if projects.any?
257 258 ancestors = []
258 259 original_project = @project
259 260 projects.sort_by(&:lft).each do |project|
260 261 # set the project environment to please macros.
261 262 @project = project
262 263 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
263 264 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
264 265 else
265 266 ancestors.pop
266 267 s << "</li>"
267 268 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
268 269 ancestors.pop
269 270 s << "</ul></li>\n"
270 271 end
271 272 end
272 273 classes = (ancestors.empty? ? 'root' : 'child')
273 274 s << "<li class='#{classes}'><div class='#{classes}'>"
274 275 s << h(block_given? ? capture(project, &block) : project.name)
275 276 s << "</div>\n"
276 277 ancestors << project
277 278 end
278 279 s << ("</li></ul>\n" * ancestors.size)
279 280 @project = original_project
280 281 end
281 282 s.html_safe
282 283 end
283 284
284 285 def render_page_hierarchy(pages, node=nil, options={})
285 286 content = ''
286 287 if pages[node]
287 288 content << "<ul class=\"pages-hierarchy\">\n"
288 289 pages[node].each do |page|
289 290 content << "<li>"
290 291 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
291 292 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
292 293 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
293 294 content << "</li>\n"
294 295 end
295 296 content << "</ul>\n"
296 297 end
297 298 content.html_safe
298 299 end
299 300
300 301 # Renders flash messages
301 302 def render_flash_messages
302 303 s = ''
303 304 flash.each do |k,v|
304 305 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
305 306 end
306 307 s.html_safe
307 308 end
308 309
309 310 # Renders tabs and their content
310 311 def render_tabs(tabs, selected=params[:tab])
311 312 if tabs.any?
312 313 unless tabs.detect {|tab| tab[:name] == selected}
313 314 selected = nil
314 315 end
315 316 selected ||= tabs.first[:name]
316 317 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
317 318 else
318 319 content_tag 'p', l(:label_no_data), :class => "nodata"
319 320 end
320 321 end
321 322
322 323 # Renders the project quick-jump box
323 324 def render_project_jump_box
324 325 return unless User.current.logged?
325 326 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
326 327 if projects.any?
327 328 options =
328 329 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
329 330 '<option value="" disabled="disabled">---</option>').html_safe
330 331
331 332 options << project_tree_options_for_select(projects, :selected => @project) do |p|
332 333 { :value => project_path(:id => p, :jump => current_menu_item) }
333 334 end
334 335
335 336 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
336 337 end
337 338 end
338 339
339 340 def project_tree_options_for_select(projects, options = {})
340 341 s = ''.html_safe
341 342 if options[:include_blank]
342 343 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
343 344 end
344 345 project_tree(projects) do |project, level|
345 346 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
346 347 tag_options = {:value => project.id}
347 348 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
348 349 tag_options[:selected] = 'selected'
349 350 else
350 351 tag_options[:selected] = nil
351 352 end
352 353 tag_options.merge!(yield(project)) if block_given?
353 354 s << content_tag('option', name_prefix + h(project), tag_options)
354 355 end
355 356 s.html_safe
356 357 end
357 358
358 359 # Yields the given block for each project with its level in the tree
359 360 #
360 361 # Wrapper for Project#project_tree
361 362 def project_tree(projects, &block)
362 363 Project.project_tree(projects, &block)
363 364 end
364 365
365 366 def principals_check_box_tags(name, principals)
366 367 s = ''
367 368 principals.each do |principal|
368 369 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
369 370 end
370 371 s.html_safe
371 372 end
372 373
373 374 # Returns a string for users/groups option tags
374 375 def principals_options_for_select(collection, selected=nil)
375 376 s = ''
376 377 if collection.include?(User.current)
377 378 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
378 379 end
379 380 groups = ''
380 381 collection.sort.each do |element|
381 382 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
382 383 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
383 384 end
384 385 unless groups.empty?
385 386 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
386 387 end
387 388 s.html_safe
388 389 end
389 390
390 391 def option_tag(name, text, value, selected=nil, options={})
391 392 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
392 393 end
393 394
394 395 def truncate_single_line_raw(string, length)
395 396 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
396 397 end
397 398
398 399 # Truncates at line break after 250 characters or options[:length]
399 400 def truncate_lines(string, options={})
400 401 length = options[:length] || 250
401 402 if string.to_s =~ /\A(.{#{length}}.*?)$/m
402 403 "#{$1}..."
403 404 else
404 405 string
405 406 end
406 407 end
407 408
408 409 def anchor(text)
409 410 text.to_s.gsub(' ', '_')
410 411 end
411 412
412 413 def html_hours(text)
413 414 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
414 415 end
415 416
416 417 def authoring(created, author, options={})
417 418 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
418 419 end
419 420
420 421 def time_tag(time)
421 422 text = distance_of_time_in_words(Time.now, time)
422 423 if @project
423 424 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
424 425 else
425 426 content_tag('abbr', text, :title => format_time(time))
426 427 end
427 428 end
428 429
429 430 def syntax_highlight_lines(name, content)
430 431 lines = []
431 432 syntax_highlight(name, content).each_line { |line| lines << line }
432 433 lines
433 434 end
434 435
435 436 def syntax_highlight(name, content)
436 437 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
437 438 end
438 439
439 440 def to_path_param(path)
440 441 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
441 442 str.blank? ? nil : str
442 443 end
443 444
444 445 def reorder_links(name, url, method = :post)
445 446 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
446 447 url.merge({"#{name}[move_to]" => 'highest'}),
447 448 :method => method, :title => l(:label_sort_highest)) +
448 449 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
449 450 url.merge({"#{name}[move_to]" => 'higher'}),
450 451 :method => method, :title => l(:label_sort_higher)) +
451 452 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
452 453 url.merge({"#{name}[move_to]" => 'lower'}),
453 454 :method => method, :title => l(:label_sort_lower)) +
454 455 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
455 456 url.merge({"#{name}[move_to]" => 'lowest'}),
456 457 :method => method, :title => l(:label_sort_lowest))
457 458 end
458 459
459 460 def breadcrumb(*args)
460 461 elements = args.flatten
461 462 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
462 463 end
463 464
464 465 def other_formats_links(&block)
465 466 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
466 467 yield Redmine::Views::OtherFormatsBuilder.new(self)
467 468 concat('</p>'.html_safe)
468 469 end
469 470
470 471 def page_header_title
471 472 if @project.nil? || @project.new_record?
472 473 h(Setting.app_title)
473 474 else
474 475 b = []
475 476 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
476 477 if ancestors.any?
477 478 root = ancestors.shift
478 479 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
479 480 if ancestors.size > 2
480 481 b << "\xe2\x80\xa6"
481 482 ancestors = ancestors[-2, 2]
482 483 end
483 484 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
484 485 end
485 486 b << h(@project)
486 487 b.join(" \xc2\xbb ").html_safe
487 488 end
488 489 end
489 490
490 491 # Returns a h2 tag and sets the html title with the given arguments
491 492 def title(*args)
492 493 strings = args.map do |arg|
493 494 if arg.is_a?(Array) && arg.size >= 2
494 495 link_to(*arg)
495 496 else
496 497 h(arg.to_s)
497 498 end
498 499 end
499 500 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
500 501 content_tag('h2', strings.join(' &#187; ').html_safe)
501 502 end
502 503
503 504 # Sets the html title
504 505 # Returns the html title when called without arguments
505 506 # Current project name and app_title and automatically appended
506 507 # Exemples:
507 508 # html_title 'Foo', 'Bar'
508 509 # html_title # => 'Foo - Bar - My Project - Redmine'
509 510 def html_title(*args)
510 511 if args.empty?
511 512 title = @html_title || []
512 513 title << @project.name if @project
513 514 title << Setting.app_title unless Setting.app_title == title.last
514 515 title.reject(&:blank?).join(' - ')
515 516 else
516 517 @html_title ||= []
517 518 @html_title += args
518 519 end
519 520 end
520 521
521 522 # Returns the theme, controller name, and action as css classes for the
522 523 # HTML body.
523 524 def body_css_classes
524 525 css = []
525 526 if theme = Redmine::Themes.theme(Setting.ui_theme)
526 527 css << 'theme-' + theme.name
527 528 end
528 529
529 530 css << 'project-' + @project.identifier if @project && @project.identifier.present?
530 531 css << 'controller-' + controller_name
531 532 css << 'action-' + action_name
532 533 css.join(' ')
533 534 end
534 535
535 536 def accesskey(s)
536 537 @used_accesskeys ||= []
537 538 key = Redmine::AccessKeys.key_for(s)
538 539 return nil if @used_accesskeys.include?(key)
539 540 @used_accesskeys << key
540 541 key
541 542 end
542 543
543 544 # Formats text according to system settings.
544 545 # 2 ways to call this method:
545 546 # * with a String: textilizable(text, options)
546 547 # * with an object and one of its attribute: textilizable(issue, :description, options)
547 548 def textilizable(*args)
548 549 options = args.last.is_a?(Hash) ? args.pop : {}
549 550 case args.size
550 551 when 1
551 552 obj = options[:object]
552 553 text = args.shift
553 554 when 2
554 555 obj = args.shift
555 556 attr = args.shift
556 557 text = obj.send(attr).to_s
557 558 else
558 559 raise ArgumentError, 'invalid arguments to textilizable'
559 560 end
560 561 return '' if text.blank?
561 562 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
562 563 @only_path = only_path = options.delete(:only_path) == false ? false : true
563 564
564 565 text = text.dup
565 566 macros = catch_macros(text)
566 567 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
567 568
568 569 @parsed_headings = []
569 570 @heading_anchors = {}
570 571 @current_section = 0 if options[:edit_section_links]
571 572
572 573 parse_sections(text, project, obj, attr, only_path, options)
573 574 text = parse_non_pre_blocks(text, obj, macros) do |text|
574 575 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
575 576 send method_name, text, project, obj, attr, only_path, options
576 577 end
577 578 end
578 579 parse_headings(text, project, obj, attr, only_path, options)
579 580
580 581 if @parsed_headings.any?
581 582 replace_toc(text, @parsed_headings)
582 583 end
583 584
584 585 text.html_safe
585 586 end
586 587
587 588 def parse_non_pre_blocks(text, obj, macros)
588 589 s = StringScanner.new(text)
589 590 tags = []
590 591 parsed = ''
591 592 while !s.eos?
592 593 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
593 594 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
594 595 if tags.empty?
595 596 yield text
596 597 inject_macros(text, obj, macros) if macros.any?
597 598 else
598 599 inject_macros(text, obj, macros, false) if macros.any?
599 600 end
600 601 parsed << text
601 602 if tag
602 603 if closing
603 604 if tags.last == tag.downcase
604 605 tags.pop
605 606 end
606 607 else
607 608 tags << tag.downcase
608 609 end
609 610 parsed << full_tag
610 611 end
611 612 end
612 613 # Close any non closing tags
613 614 while tag = tags.pop
614 615 parsed << "</#{tag}>"
615 616 end
616 617 parsed
617 618 end
618 619
619 620 def parse_inline_attachments(text, project, obj, attr, only_path, options)
620 621 # when using an image link, try to use an attachment, if possible
621 622 attachments = options[:attachments] || []
622 623 attachments += obj.attachments if obj.respond_to?(:attachments)
623 624 if attachments.present?
624 625 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
625 626 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
626 627 # search for the picture in attachments
627 628 if found = Attachment.latest_attach(attachments, filename)
628 629 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
629 630 desc = found.description.to_s.gsub('"', '')
630 631 if !desc.blank? && alttext.blank?
631 632 alt = " title=\"#{desc}\" alt=\"#{desc}\""
632 633 end
633 634 "src=\"#{image_url}\"#{alt}"
634 635 else
635 636 m
636 637 end
637 638 end
638 639 end
639 640 end
640 641
641 642 # Wiki links
642 643 #
643 644 # Examples:
644 645 # [[mypage]]
645 646 # [[mypage|mytext]]
646 647 # wiki links can refer other project wikis, using project name or identifier:
647 648 # [[project:]] -> wiki starting page
648 649 # [[project:|mytext]]
649 650 # [[project:mypage]]
650 651 # [[project:mypage|mytext]]
651 652 def parse_wiki_links(text, project, obj, attr, only_path, options)
652 653 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
653 654 link_project = project
654 655 esc, all, page, title = $1, $2, $3, $5
655 656 if esc.nil?
656 657 if page =~ /^([^\:]+)\:(.*)$/
657 658 identifier, page = $1, $2
658 659 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
659 660 title ||= identifier if page.blank?
660 661 end
661 662
662 663 if link_project && link_project.wiki
663 664 # extract anchor
664 665 anchor = nil
665 666 if page =~ /^(.+?)\#(.+)$/
666 667 page, anchor = $1, $2
667 668 end
668 669 anchor = sanitize_anchor_name(anchor) if anchor.present?
669 670 # check if page exists
670 671 wiki_page = link_project.wiki.find_page(page)
671 672 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
672 673 "##{anchor}"
673 674 else
674 675 case options[:wiki_links]
675 676 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
676 677 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
677 678 else
678 679 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
679 680 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
680 681 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
681 682 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
682 683 end
683 684 end
684 685 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
685 686 else
686 687 # project or wiki doesn't exist
687 688 all
688 689 end
689 690 else
690 691 all
691 692 end
692 693 end
693 694 end
694 695
695 696 # Redmine links
696 697 #
697 698 # Examples:
698 699 # Issues:
699 700 # #52 -> Link to issue #52
700 701 # Changesets:
701 702 # r52 -> Link to revision 52
702 703 # commit:a85130f -> Link to scmid starting with a85130f
703 704 # Documents:
704 705 # document#17 -> Link to document with id 17
705 706 # document:Greetings -> Link to the document with title "Greetings"
706 707 # document:"Some document" -> Link to the document with title "Some document"
707 708 # Versions:
708 709 # version#3 -> Link to version with id 3
709 710 # version:1.0.0 -> Link to version named "1.0.0"
710 711 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
711 712 # Attachments:
712 713 # attachment:file.zip -> Link to the attachment of the current object named file.zip
713 714 # Source files:
714 715 # source:some/file -> Link to the file located at /some/file in the project's repository
715 716 # source:some/file@52 -> Link to the file's revision 52
716 717 # source:some/file#L120 -> Link to line 120 of the file
717 718 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
718 719 # export:some/file -> Force the download of the file
719 720 # Forum messages:
720 721 # message#1218 -> Link to message with id 1218
721 722 # Projects:
722 723 # project:someproject -> Link to project named "someproject"
723 724 # project#3 -> Link to project with id 3
724 725 #
725 726 # Links can refer other objects from other projects, using project identifier:
726 727 # identifier:r52
727 728 # identifier:document:"Some document"
728 729 # identifier:version:1.0.0
729 730 # identifier:source:some/file
730 731 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
731 732 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|
732 733 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
733 734 if tag_content
734 735 $&
735 736 else
736 737 link = nil
737 738 project = default_project
738 739 if project_identifier
739 740 project = Project.visible.find_by_identifier(project_identifier)
740 741 end
741 742 if esc.nil?
742 743 if prefix.nil? && sep == 'r'
743 744 if project
744 745 repository = nil
745 746 if repo_identifier
746 747 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
747 748 else
748 749 repository = project.repository
749 750 end
750 751 # project.changesets.visible raises an SQL error because of a double join on repositories
751 752 if repository &&
752 753 (changeset = Changeset.visible.
753 754 find_by_repository_id_and_revision(repository.id, identifier))
754 755 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
755 756 {:only_path => only_path, :controller => 'repositories',
756 757 :action => 'revision', :id => project,
757 758 :repository_id => repository.identifier_param,
758 759 :rev => changeset.revision},
759 760 :class => 'changeset',
760 761 :title => truncate_single_line_raw(changeset.comments, 100))
761 762 end
762 763 end
763 764 elsif sep == '#'
764 765 oid = identifier.to_i
765 766 case prefix
766 767 when nil
767 768 if oid.to_s == identifier &&
768 769 issue = Issue.visible.find_by_id(oid)
769 770 anchor = comment_id ? "note-#{comment_id}" : nil
770 771 link = link_to("##{oid}#{comment_suffix}",
771 772 issue_path(issue, :only_path => only_path, :anchor => anchor),
772 773 :class => issue.css_classes,
773 774 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
774 775 end
775 776 when 'document'
776 777 if document = Document.visible.find_by_id(oid)
777 778 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
778 779 end
779 780 when 'version'
780 781 if version = Version.visible.find_by_id(oid)
781 782 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
782 783 end
783 784 when 'message'
784 785 if message = Message.visible.find_by_id(oid)
785 786 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
786 787 end
787 788 when 'forum'
788 789 if board = Board.visible.find_by_id(oid)
789 790 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
790 791 end
791 792 when 'news'
792 793 if news = News.visible.find_by_id(oid)
793 794 link = link_to(news.title, news_path(news, :only_path => only_path), :class => 'news')
794 795 end
795 796 when 'project'
796 797 if p = Project.visible.find_by_id(oid)
797 798 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
798 799 end
799 800 end
800 801 elsif sep == ':'
801 802 # removes the double quotes if any
802 803 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
803 804 name = CGI.unescapeHTML(name)
804 805 case prefix
805 806 when 'document'
806 807 if project && document = project.documents.visible.find_by_title(name)
807 808 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
808 809 end
809 810 when 'version'
810 811 if project && version = project.versions.visible.find_by_name(name)
811 812 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
812 813 end
813 814 when 'forum'
814 815 if project && board = project.boards.visible.find_by_name(name)
815 816 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
816 817 end
817 818 when 'news'
818 819 if project && news = project.news.visible.find_by_title(name)
819 820 link = link_to(news.title, news_path(news, :only_path => only_path), :class => 'news')
820 821 end
821 822 when 'commit', 'source', 'export'
822 823 if project
823 824 repository = nil
824 825 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
825 826 repo_prefix, repo_identifier, name = $1, $2, $3
826 827 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
827 828 else
828 829 repository = project.repository
829 830 end
830 831 if prefix == 'commit'
831 832 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
832 833 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},
833 834 :class => 'changeset',
834 835 :title => truncate_single_line_raw(changeset.comments, 100)
835 836 end
836 837 else
837 838 if repository && User.current.allowed_to?(:browse_repository, project)
838 839 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
839 840 path, rev, anchor = $1, $3, $5
840 841 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,
841 842 :path => to_path_param(path),
842 843 :rev => rev,
843 844 :anchor => anchor},
844 845 :class => (prefix == 'export' ? 'source download' : 'source')
845 846 end
846 847 end
847 848 repo_prefix = nil
848 849 end
849 850 when 'attachment'
850 851 attachments = options[:attachments] || []
851 852 attachments += obj.attachments if obj.respond_to?(:attachments)
852 853 if attachments && attachment = Attachment.latest_attach(attachments, name)
853 854 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
854 855 end
855 856 when 'project'
856 857 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
857 858 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
858 859 end
859 860 end
860 861 end
861 862 end
862 863 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
863 864 end
864 865 end
865 866 end
866 867
867 868 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
868 869
869 870 def parse_sections(text, project, obj, attr, only_path, options)
870 871 return unless options[:edit_section_links]
871 872 text.gsub!(HEADING_RE) do
872 873 heading = $1
873 874 @current_section += 1
874 875 if @current_section > 1
875 876 content_tag('div',
876 877 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
877 878 :class => 'contextual',
878 879 :title => l(:button_edit_section),
879 880 :id => "section-#{@current_section}") + heading.html_safe
880 881 else
881 882 heading
882 883 end
883 884 end
884 885 end
885 886
886 887 # Headings and TOC
887 888 # Adds ids and links to headings unless options[:headings] is set to false
888 889 def parse_headings(text, project, obj, attr, only_path, options)
889 890 return if options[:headings] == false
890 891
891 892 text.gsub!(HEADING_RE) do
892 893 level, attrs, content = $2.to_i, $3, $4
893 894 item = strip_tags(content).strip
894 895 anchor = sanitize_anchor_name(item)
895 896 # used for single-file wiki export
896 897 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
897 898 @heading_anchors[anchor] ||= 0
898 899 idx = (@heading_anchors[anchor] += 1)
899 900 if idx > 1
900 901 anchor = "#{anchor}-#{idx}"
901 902 end
902 903 @parsed_headings << [level, anchor, item]
903 904 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
904 905 end
905 906 end
906 907
907 908 MACROS_RE = /(
908 909 (!)? # escaping
909 910 (
910 911 \{\{ # opening tag
911 912 ([\w]+) # macro name
912 913 (\(([^\n\r]*?)\))? # optional arguments
913 914 ([\n\r].*?[\n\r])? # optional block of text
914 915 \}\} # closing tag
915 916 )
916 917 )/mx unless const_defined?(:MACROS_RE)
917 918
918 919 MACRO_SUB_RE = /(
919 920 \{\{
920 921 macro\((\d+)\)
921 922 \}\}
922 923 )/x unless const_defined?(:MACRO_SUB_RE)
923 924
924 925 # Extracts macros from text
925 926 def catch_macros(text)
926 927 macros = {}
927 928 text.gsub!(MACROS_RE) do
928 929 all, macro = $1, $4.downcase
929 930 if macro_exists?(macro) || all =~ MACRO_SUB_RE
930 931 index = macros.size
931 932 macros[index] = all
932 933 "{{macro(#{index})}}"
933 934 else
934 935 all
935 936 end
936 937 end
937 938 macros
938 939 end
939 940
940 941 # Executes and replaces macros in text
941 942 def inject_macros(text, obj, macros, execute=true)
942 943 text.gsub!(MACRO_SUB_RE) do
943 944 all, index = $1, $2.to_i
944 945 orig = macros.delete(index)
945 946 if execute && orig && orig =~ MACROS_RE
946 947 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
947 948 if esc.nil?
948 949 h(exec_macro(macro, obj, args, block) || all)
949 950 else
950 951 h(all)
951 952 end
952 953 elsif orig
953 954 h(orig)
954 955 else
955 956 h(all)
956 957 end
957 958 end
958 959 end
959 960
960 961 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
961 962
962 963 # Renders the TOC with given headings
963 964 def replace_toc(text, headings)
964 965 text.gsub!(TOC_RE) do
965 966 left_align, right_align = $2, $3
966 967 # Keep only the 4 first levels
967 968 headings = headings.select{|level, anchor, item| level <= 4}
968 969 if headings.empty?
969 970 ''
970 971 else
971 972 div_class = 'toc'
972 973 div_class << ' right' if right_align
973 974 div_class << ' left' if left_align
974 975 out = "<ul class=\"#{div_class}\"><li>"
975 976 root = headings.map(&:first).min
976 977 current = root
977 978 started = false
978 979 headings.each do |level, anchor, item|
979 980 if level > current
980 981 out << '<ul><li>' * (level - current)
981 982 elsif level < current
982 983 out << "</li></ul>\n" * (current - level) + "</li><li>"
983 984 elsif started
984 985 out << '</li><li>'
985 986 end
986 987 out << "<a href=\"##{anchor}\">#{item}</a>"
987 988 current = level
988 989 started = true
989 990 end
990 991 out << '</li></ul>' * (current - root)
991 992 out << '</li></ul>'
992 993 end
993 994 end
994 995 end
995 996
996 997 # Same as Rails' simple_format helper without using paragraphs
997 998 def simple_format_without_paragraph(text)
998 999 text.to_s.
999 1000 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1000 1001 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1001 1002 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1002 1003 html_safe
1003 1004 end
1004 1005
1005 1006 def lang_options_for_select(blank=true)
1006 1007 (blank ? [["(auto)", ""]] : []) + languages_options
1007 1008 end
1008 1009
1009 1010 def labelled_form_for(*args, &proc)
1010 1011 args << {} unless args.last.is_a?(Hash)
1011 1012 options = args.last
1012 1013 if args.first.is_a?(Symbol)
1013 1014 options.merge!(:as => args.shift)
1014 1015 end
1015 1016 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1016 1017 form_for(*args, &proc)
1017 1018 end
1018 1019
1019 1020 def labelled_fields_for(*args, &proc)
1020 1021 args << {} unless args.last.is_a?(Hash)
1021 1022 options = args.last
1022 1023 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1023 1024 fields_for(*args, &proc)
1024 1025 end
1025 1026
1026 1027 def error_messages_for(*objects)
1027 1028 html = ""
1028 1029 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1029 1030 errors = objects.map {|o| o.errors.full_messages}.flatten
1030 1031 if errors.any?
1031 1032 html << "<div id='errorExplanation'><ul>\n"
1032 1033 errors.each do |error|
1033 1034 html << "<li>#{h error}</li>\n"
1034 1035 end
1035 1036 html << "</ul></div>\n"
1036 1037 end
1037 1038 html.html_safe
1038 1039 end
1039 1040
1040 1041 def delete_link(url, options={})
1041 1042 options = {
1042 1043 :method => :delete,
1043 1044 :data => {:confirm => l(:text_are_you_sure)},
1044 1045 :class => 'icon icon-del'
1045 1046 }.merge(options)
1046 1047
1047 1048 link_to l(:button_delete), url, options
1048 1049 end
1049 1050
1050 1051 def preview_link(url, form, target='preview', options={})
1051 1052 content_tag 'a', l(:label_preview), {
1052 1053 :href => "#",
1053 1054 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1054 1055 :accesskey => accesskey(:preview)
1055 1056 }.merge(options)
1056 1057 end
1057 1058
1058 1059 def link_to_function(name, function, html_options={})
1059 1060 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1060 1061 end
1061 1062
1062 1063 # Helper to render JSON in views
1063 1064 def raw_json(arg)
1064 1065 arg.to_json.to_s.gsub('/', '\/').html_safe
1065 1066 end
1066 1067
1067 1068 def back_url
1068 1069 url = params[:back_url]
1069 1070 if url.nil? && referer = request.env['HTTP_REFERER']
1070 1071 url = CGI.unescape(referer.to_s)
1071 1072 end
1072 1073 url
1073 1074 end
1074 1075
1075 1076 def back_url_hidden_field_tag
1076 1077 url = back_url
1077 1078 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1078 1079 end
1079 1080
1080 1081 def check_all_links(form_name)
1081 1082 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1082 1083 " | ".html_safe +
1083 1084 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1084 1085 end
1085 1086
1086 1087 def toggle_checkboxes_link(selector)
1087 1088 link_to_function image_tag('toggle_check.png'),
1088 1089 "toggleCheckboxesBySelector('#{selector}')",
1089 1090 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1090 1091 end
1091 1092
1092 1093 def progress_bar(pcts, options={})
1093 1094 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1094 1095 pcts = pcts.collect(&:round)
1095 1096 pcts[1] = pcts[1] - pcts[0]
1096 1097 pcts << (100 - pcts[1] - pcts[0])
1097 1098 width = options[:width] || '100px;'
1098 1099 legend = options[:legend] || ''
1099 1100 content_tag('table',
1100 1101 content_tag('tr',
1101 1102 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1102 1103 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1103 1104 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1104 1105 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1105 1106 content_tag('p', legend, :class => 'percent').html_safe
1106 1107 end
1107 1108
1108 1109 def checked_image(checked=true)
1109 1110 if checked
1110 1111 image_tag 'toggle_check.png'
1111 1112 end
1112 1113 end
1113 1114
1114 1115 def context_menu(url)
1115 1116 unless @context_menu_included
1116 1117 content_for :header_tags do
1117 1118 javascript_include_tag('context_menu') +
1118 1119 stylesheet_link_tag('context_menu')
1119 1120 end
1120 1121 if l(:direction) == 'rtl'
1121 1122 content_for :header_tags do
1122 1123 stylesheet_link_tag('context_menu_rtl')
1123 1124 end
1124 1125 end
1125 1126 @context_menu_included = true
1126 1127 end
1127 1128 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1128 1129 end
1129 1130
1130 1131 def calendar_for(field_id)
1131 1132 include_calendar_headers_tags
1132 1133 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1133 1134 end
1134 1135
1135 1136 def include_calendar_headers_tags
1136 1137 unless @calendar_headers_tags_included
1137 1138 tags = ''.html_safe
1138 1139 @calendar_headers_tags_included = true
1139 1140 content_for :header_tags do
1140 1141 start_of_week = Setting.start_of_week
1141 1142 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1142 1143 # Redmine uses 1..7 (monday..sunday) in settings and locales
1143 1144 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1144 1145 start_of_week = start_of_week.to_i % 7
1145 1146 tags << javascript_tag(
1146 1147 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1147 1148 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1148 1149 path_to_image('/images/calendar.png') +
1149 1150 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1150 1151 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1151 1152 "beforeShow: beforeShowDatePicker};")
1152 1153 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1153 1154 unless jquery_locale == 'en'
1154 1155 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1155 1156 end
1156 1157 tags
1157 1158 end
1158 1159 end
1159 1160 end
1160 1161
1161 1162 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1162 1163 # Examples:
1163 1164 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1164 1165 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1165 1166 #
1166 1167 def stylesheet_link_tag(*sources)
1167 1168 options = sources.last.is_a?(Hash) ? sources.pop : {}
1168 1169 plugin = options.delete(:plugin)
1169 1170 sources = sources.map do |source|
1170 1171 if plugin
1171 1172 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1172 1173 elsif current_theme && current_theme.stylesheets.include?(source)
1173 1174 current_theme.stylesheet_path(source)
1174 1175 else
1175 1176 source
1176 1177 end
1177 1178 end
1178 1179 super *sources, options
1179 1180 end
1180 1181
1181 1182 # Overrides Rails' image_tag with themes and plugins support.
1182 1183 # Examples:
1183 1184 # image_tag('image.png') # => picks image.png from the current theme or defaults
1184 1185 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1185 1186 #
1186 1187 def image_tag(source, options={})
1187 1188 if plugin = options.delete(:plugin)
1188 1189 source = "/plugin_assets/#{plugin}/images/#{source}"
1189 1190 elsif current_theme && current_theme.images.include?(source)
1190 1191 source = current_theme.image_path(source)
1191 1192 end
1192 1193 super source, options
1193 1194 end
1194 1195
1195 1196 # Overrides Rails' javascript_include_tag with plugins support
1196 1197 # Examples:
1197 1198 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1198 1199 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1199 1200 #
1200 1201 def javascript_include_tag(*sources)
1201 1202 options = sources.last.is_a?(Hash) ? sources.pop : {}
1202 1203 if plugin = options.delete(:plugin)
1203 1204 sources = sources.map do |source|
1204 1205 if plugin
1205 1206 "/plugin_assets/#{plugin}/javascripts/#{source}"
1206 1207 else
1207 1208 source
1208 1209 end
1209 1210 end
1210 1211 end
1211 1212 super *sources, options
1212 1213 end
1213 1214
1214 1215 def sidebar_content?
1215 1216 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1216 1217 end
1217 1218
1218 1219 def view_layouts_base_sidebar_hook_response
1219 1220 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1220 1221 end
1221 1222
1222 1223 def email_delivery_enabled?
1223 1224 !!ActionMailer::Base.perform_deliveries
1224 1225 end
1225 1226
1226 1227 # Returns the avatar image tag for the given +user+ if avatars are enabled
1227 1228 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1228 1229 def avatar(user, options = { })
1229 1230 if Setting.gravatar_enabled?
1230 1231 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1231 1232 email = nil
1232 1233 if user.respond_to?(:mail)
1233 1234 email = user.mail
1234 1235 elsif user.to_s =~ %r{<(.+?)>}
1235 1236 email = $1
1236 1237 end
1237 1238 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1238 1239 else
1239 1240 ''
1240 1241 end
1241 1242 end
1242 1243
1243 1244 def sanitize_anchor_name(anchor)
1244 1245 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1245 1246 end
1246 1247
1247 1248 # Returns the javascript tags that are included in the html layout head
1248 1249 def javascript_heads
1249 1250 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1250 1251 unless User.current.pref.warn_on_leaving_unsaved == '0'
1251 1252 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1252 1253 end
1253 1254 tags
1254 1255 end
1255 1256
1256 1257 def favicon
1257 1258 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1258 1259 end
1259 1260
1260 1261 # Returns the path to the favicon
1261 1262 def favicon_path
1262 1263 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1263 1264 image_path(icon)
1264 1265 end
1265 1266
1266 1267 # Returns the full URL to the favicon
1267 1268 def favicon_url
1268 1269 # TODO: use #image_url introduced in Rails4
1269 1270 path = favicon_path
1270 1271 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1271 1272 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1272 1273 end
1273 1274
1274 1275 def robot_exclusion_tag
1275 1276 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1276 1277 end
1277 1278
1278 1279 # Returns true if arg is expected in the API response
1279 1280 def include_in_api_response?(arg)
1280 1281 unless @included_in_api_response
1281 1282 param = params[:include]
1282 1283 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1283 1284 @included_in_api_response.collect!(&:strip)
1284 1285 end
1285 1286 @included_in_api_response.include?(arg.to_s)
1286 1287 end
1287 1288
1288 1289 # Returns options or nil if nometa param or X-Redmine-Nometa header
1289 1290 # was set in the request
1290 1291 def api_meta(options)
1291 1292 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1292 1293 # compatibility mode for activeresource clients that raise
1293 1294 # an error when deserializing an array with attributes
1294 1295 nil
1295 1296 else
1296 1297 options
1297 1298 end
1298 1299 end
1299 1300
1300 1301 private
1301 1302
1302 1303 def wiki_helper
1303 1304 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1304 1305 extend helper
1305 1306 return self
1306 1307 end
1307 1308
1308 1309 def link_to_content_update(text, url_params = {}, html_options = {})
1309 1310 link_to(text, url_params, html_options)
1310 1311 end
1311 1312 end
@@ -1,58 +1,58
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 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 module CalendarsHelper
21 21 def link_to_previous_month(year, month, options={})
22 22 target_year, target_month = if month == 1
23 23 [year - 1, 12]
24 24 else
25 25 [year, month - 1]
26 26 end
27 27
28 28 name = if target_month == 12
29 29 "#{month_name(target_month)} #{target_year}"
30 30 else
31 31 "#{month_name(target_month)}"
32 32 end
33 33
34 34 # \xc2\xab(utf-8) = &#171;
35 35 link_to_month(("\xc2\xab " + name), target_year, target_month, options)
36 36 end
37 37
38 38 def link_to_next_month(year, month, options={})
39 39 target_year, target_month = if month == 12
40 40 [year + 1, 1]
41 41 else
42 42 [year, month + 1]
43 43 end
44 44
45 45 name = if target_month == 1
46 46 "#{month_name(target_month)} #{target_year}"
47 47 else
48 48 "#{month_name(target_month)}"
49 49 end
50 50
51 51 # \xc2\xbb(utf-8) = &#187;
52 52 link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
53 53 end
54 54
55 55 def link_to_month(link_name, year, month, options={})
56 link_to_content_update(h(link_name), params.merge(:year => year, :month => month))
56 link_to_content_update(h(link_name), params.merge(:year => year, :month => month), options)
57 57 end
58 58 end
@@ -1,66 +1,68
1 1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)).html_safe %></h2>
2 2 <p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
3 3
4 4 <div id="activity">
5 5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 6 <h3><%= format_activity_day(day) %></h3>
7 7 <dl>
8 8 <% sort_activity_events(@events_by_day[day]).each do |e, in_group| -%>
9 9 <dt class="<%= e.event_type %> <%= "grouped" if in_group %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
11 11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
12 12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
13 13 <%= link_to format_activity_title(e.event_title), e.event_url %>
14 14 </dt>
15 15 <dd class="<%= "grouped" if in_group %>"><span class="description"><%= format_activity_description(e.event_description) %></span>
16 16 <span class="author"><%= link_to_user(e.event_author) if e.respond_to?(:event_author) %></span></dd>
17 17 <% end -%>
18 18 </dl>
19 19 <% end -%>
20 20 </div>
21 21
22 22 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
23 23
24 24 <div style="float:left;">
25 25 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
26 26 params.merge(:from => @date_to - @days - 1),
27 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %>
27 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1)),
28 :accesskey => accesskey(:previous)) %>
28 29 </div>
29 30 <div style="float:right;">
30 31 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
31 32 params.merge(:from => @date_to + @days - 1),
32 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %>
33 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1)),
34 :accesskey => accesskey(:next)) unless @date_to >= Date.today %>
33 35 </div>
34 36 &nbsp;
35 37 <% other_formats_links do |f| %>
36 38 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
37 39 <% end %>
38 40
39 41 <% content_for :header_tags do %>
40 42 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
41 43 <% end %>
42 44
43 45 <% content_for :sidebar do %>
44 46 <%= form_tag({}, :method => :get) do %>
45 47 <h3><%= l(:label_activity) %></h3>
46 48 <ul>
47 49 <% @activity.event_types.each do |t| %>
48 50 <li>
49 51 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
50 52 <label for="show_<%=t%>">
51 53 <%= link_to(l("label_#{t.singularize}_plural"),
52 54 {"show_#{t}" => 1, :user_id => params[:user_id], :from => params[:from]})%>
53 55 </label>
54 56 </li>
55 57 <% end %>
56 58 </ul>
57 59 <% if @project && @project.descendants.active.any? %>
58 60 <%= hidden_field_tag 'with_subprojects', 0, :id => nil %>
59 61 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
60 62 <% end %>
61 63 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
62 64 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
63 65 <% end %>
64 66 <% end %>
65 67
66 68 <% html_title(l(:label_activity), @author) -%>
@@ -1,45 +1,45
1 1 <h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
2 2
3 3 <%= form_tag({:controller => 'calendars', :action => 'show', :project_id => @project},
4 4 :method => :get, :id => 'query_form') do %>
5 5 <%= hidden_field_tag 'set_filter', '1' %>
6 6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
7 7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
8 8 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
9 9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
10 10 </div>
11 11 </fieldset>
12 12
13 13 <p style="float:right;">
14 <%= link_to_previous_month(@year, @month) %> | <%= link_to_next_month(@year, @month) %>
14 <%= link_to_previous_month(@year, @month, :accesskey => accesskey(:previous)) %> | <%= link_to_next_month(@year, @month, :accesskey => accesskey(:next)) %>
15 15 </p>
16 16
17 17 <p class="buttons">
18 18 <%= label_tag('month', l(:label_month)) %>
19 19 <%= select_month(@month, :prefix => "month", :discard_type => true) %>
20 20 <%= label_tag('year', l(:label_year)) %>
21 21 <%= select_year(@year, :prefix => "year", :discard_type => true) %>
22 22
23 23 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
24 24 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
25 25 </p>
26 26 <% end %>
27 27
28 28 <%= error_messages_for 'query' %>
29 29 <% if @query.valid? %>
30 30 <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
31 31
32 32 <%= call_hook(:view_calendars_show_bottom, :year => @year, :month => @month, :project => @project, :query => @query) %>
33 33
34 34 <p class="legend cal">
35 35 <span class="starting"><%= l(:text_tip_issue_begin_day) %></span>
36 36 <span class="ending"><%= l(:text_tip_issue_end_day) %></span>
37 37 <span class="starting ending"><%= l(:text_tip_issue_begin_end_day) %></span>
38 38 </p>
39 39 <% end %>
40 40
41 41 <% content_for :sidebar do %>
42 42 <%= render :partial => 'issues/sidebar' %>
43 43 <% end %>
44 44
45 45 <% html_title(l(:label_calendar)) -%>
@@ -1,332 +1,334
1 1 <% @gantt.view = self %>
2 2 <div class="contextual">
3 3 <% if !@query.new_record? && @query.editable_by?(User.current) %>
4 4 <%= link_to l(:button_edit), edit_query_path(@query, :gantt => 1), :class => 'icon icon-edit' %>
5 5 <%= delete_link query_path(@query, :gantt => 1) %>
6 6 <% end %>
7 7 </div>
8 8
9 9 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
10 10
11 11 <%= form_tag({:controller => 'gantts', :action => 'show',
12 12 :project_id => @project, :month => params[:month],
13 13 :year => params[:year], :months => params[:months]},
14 14 :method => :get, :id => 'query_form') do %>
15 15 <%= hidden_field_tag 'set_filter', '1' %>
16 16 <%= hidden_field_tag 'gantt', '1' %>
17 17 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
18 18 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
19 19 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
20 20 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
21 21 </div>
22 22 </fieldset>
23 23 <fieldset class="collapsible collapsed">
24 24 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
25 25 <div style="display: none;">
26 26 <table>
27 27 <tr>
28 28 <td>
29 29 <fieldset>
30 30 <legend><%= l(:label_related_issues) %></legend>
31 31 <label for="draw_relations">
32 32 <%= check_box 'query', 'draw_relations', :id => 'draw_relations' %>
33 33 <% rels = [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES] %>
34 34 <% rels.each do |rel| %>
35 35 <% color = Redmine::Helpers::Gantt::DRAW_TYPES[rel][:color] %>
36 36 <%= content_tag(:span, '&nbsp;&nbsp;&nbsp;'.html_safe,
37 37 :style => "background-color: #{color}") %>
38 38 <%= l(IssueRelation::TYPES[rel][:name]) %>
39 39 <% end %>
40 40 </label>
41 41 </fieldset>
42 42 </td>
43 43 <td>
44 44 <fieldset>
45 45 <legend><%= l(:label_gantt_progress_line) %></legend>
46 46 <label for="draw_progress_line">
47 47 <%= check_box 'query', 'draw_progress_line', :id => 'draw_progress_line' %>
48 48 <%= l(:label_display) %>
49 49 </label>
50 50 </fieldset>
51 51 </td>
52 52 </tr>
53 53 </table>
54 54 </div>
55 55 </fieldset>
56 56
57 57 <p class="contextual">
58 58 <%= gantt_zoom_link(@gantt, :in) %>
59 59 <%= gantt_zoom_link(@gantt, :out) %>
60 60 </p>
61 61
62 62 <p class="buttons">
63 63 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
64 64 <%= l(:label_months_from) %>
65 65 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
66 66 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
67 67 <%= hidden_field_tag 'zoom', @gantt.zoom %>
68 68
69 69 <%= link_to_function l(:button_apply), '$("#query_form").submit()',
70 70 :class => 'icon icon-checked' %>
71 71 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
72 72 :class => 'icon icon-reload' %>
73 73 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
74 74 <%= link_to_function l(:button_save),
75 75 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit();",
76 76 :class => 'icon icon-save' %>
77 77 <% end %>
78 78 </p>
79 79 <% end %>
80 80
81 81 <%= error_messages_for 'query' %>
82 82 <% if @query.valid? %>
83 83 <%
84 84 zoom = 1
85 85 @gantt.zoom.times { zoom = zoom * 2 }
86 86
87 87 subject_width = 330
88 88 header_height = 18
89 89
90 90 headers_height = header_height
91 91 show_weeks = false
92 92 show_days = false
93 93
94 94 if @gantt.zoom > 1
95 95 show_weeks = true
96 96 headers_height = 2 * header_height
97 97 if @gantt.zoom > 2
98 98 show_days = true
99 99 headers_height = 3 * header_height
100 100 end
101 101 end
102 102
103 103 # Width of the entire chart
104 104 g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i
105 105 @gantt.render(:top => headers_height + 8,
106 106 :zoom => zoom,
107 107 :g_width => g_width,
108 108 :subject_width => subject_width)
109 109 g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
110 110 t_height = g_height + headers_height
111 111 %>
112 112
113 113 <% if @gantt.truncated %>
114 114 <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
115 115 <% end %>
116 116
117 117 <table style="width:100%; border:0; border-collapse: collapse;">
118 118 <tr>
119 119 <td style="width:<%= subject_width %>px; padding:0px;">
120 120 <%
121 121 style = ""
122 122 style += "position:relative;"
123 123 style += "height: #{t_height + 24}px;"
124 124 style += "width: #{subject_width + 1}px;"
125 125 %>
126 126 <%= content_tag(:div, :style => style) do %>
127 127 <%
128 128 style = ""
129 129 style += "right:-2px;"
130 130 style += "width: #{subject_width}px;"
131 131 style += "height: #{headers_height}px;"
132 132 style += 'background: #eee;'
133 133 %>
134 134 <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
135 135 <%
136 136 style = ""
137 137 style += "right:-2px;"
138 138 style += "width: #{subject_width}px;"
139 139 style += "height: #{t_height}px;"
140 140 style += 'border-left: 1px solid #c0c0c0;'
141 141 style += 'overflow: hidden;'
142 142 %>
143 143 <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
144 144 <%= content_tag(:div, :class => "gantt_subjects") do %>
145 145 <%= @gantt.subjects.html_safe %>
146 146 <% end %>
147 147 <% end %>
148 148 </td>
149 149
150 150 <td>
151 151 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
152 152 <%
153 153 style = ""
154 154 style += "width: #{g_width - 1}px;"
155 155 style += "height: #{headers_height}px;"
156 156 style += 'background: #eee;'
157 157 %>
158 158 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style, :class => "gantt_hdr") %>
159 159
160 160 <% ###### Months headers ###### %>
161 161 <%
162 162 month_f = @gantt.date_from
163 163 left = 0
164 164 height = (show_weeks ? header_height : header_height + g_height)
165 165 %>
166 166 <% @gantt.months.times do %>
167 167 <%
168 168 width = (((month_f >> 1) - month_f) * zoom - 1).to_i
169 169 style = ""
170 170 style += "left: #{left}px;"
171 171 style += "width: #{width}px;"
172 172 style += "height: #{height}px;"
173 173 %>
174 174 <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
175 175 <%= link_to h("#{month_f.year}-#{month_f.month}"),
176 176 @gantt.params.merge(:year => month_f.year, :month => month_f.month),
177 177 :title => "#{month_name(month_f.month)} #{month_f.year}" %>
178 178 <% end %>
179 179 <%
180 180 left = left + width + 1
181 181 month_f = month_f >> 1
182 182 %>
183 183 <% end %>
184 184
185 185 <% ###### Weeks headers ###### %>
186 186 <% if show_weeks %>
187 187 <%
188 188 left = 0
189 189 height = (show_days ? header_height - 1 : header_height - 1 + g_height)
190 190 %>
191 191 <% if @gantt.date_from.cwday == 1 %>
192 192 <%
193 193 # @date_from is monday
194 194 week_f = @gantt.date_from
195 195 %>
196 196 <% else %>
197 197 <%
198 198 # find next monday after @date_from
199 199 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
200 200 width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
201 201 style = ""
202 202 style += "left: #{left}px;"
203 203 style += "top: 19px;"
204 204 style += "width: #{width}px;"
205 205 style += "height: #{height}px;"
206 206 %>
207 207 <%= content_tag(:div, '&nbsp;'.html_safe,
208 208 :style => style, :class => "gantt_hdr") %>
209 209 <% left = left + width + 1 %>
210 210 <% end %>
211 211 <% while week_f <= @gantt.date_to %>
212 212 <%
213 213 width = ((week_f + 6 <= @gantt.date_to) ?
214 214 7 * zoom - 1 :
215 215 (@gantt.date_to - week_f + 1) * zoom - 1).to_i
216 216 style = ""
217 217 style += "left: #{left}px;"
218 218 style += "top: 19px;"
219 219 style += "width: #{width}px;"
220 220 style += "height: #{height}px;"
221 221 %>
222 222 <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
223 223 <%= content_tag(:small) do %>
224 224 <%= week_f.cweek if width >= 16 %>
225 225 <% end %>
226 226 <% end %>
227 227 <%
228 228 left = left + width + 1
229 229 week_f = week_f + 7
230 230 %>
231 231 <% end %>
232 232 <% end %>
233 233
234 234 <% ###### Days headers ####### %>
235 235 <% if show_days %>
236 236 <%
237 237 left = 0
238 238 height = g_height + header_height - 1
239 239 wday = @gantt.date_from.cwday
240 240 %>
241 241 <% (@gantt.date_to - @gantt.date_from + 1).to_i.times do %>
242 242 <%
243 243 width = zoom - 1
244 244 style = ""
245 245 style += "left: #{left}px;"
246 246 style += "top:37px;"
247 247 style += "width: #{width}px;"
248 248 style += "height: #{height}px;"
249 249 style += "font-size:0.7em;"
250 250 clss = "gantt_hdr"
251 251 clss << " nwday" if @gantt.non_working_week_days.include?(wday)
252 252 %>
253 253 <%= content_tag(:div, :style => style, :class => clss) do %>
254 254 <%= day_letter(wday) %>
255 255 <% end %>
256 256 <%
257 257 left = left + width + 1
258 258 wday = wday + 1
259 259 wday = 1 if wday > 7
260 260 %>
261 261 <% end %>
262 262 <% end %>
263 263
264 264 <%= @gantt.lines.html_safe %>
265 265
266 266 <% ###### Today red line (excluded from cache) ###### %>
267 267 <% if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
268 268 <%
269 269 today_left = (((Date.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i
270 270 style = ""
271 271 style += "position: absolute;"
272 272 style += "height: #{g_height}px;"
273 273 style += "top: #{headers_height + 1}px;"
274 274 style += "left: #{today_left}px;"
275 275 style += "width:10px;"
276 276 style += "border-left: 1px dashed red;"
277 277 %>
278 278 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style, :id => 'today_line') %>
279 279 <% end %>
280 280 <%
281 281 style = ""
282 282 style += "position: absolute;"
283 283 style += "height: #{g_height}px;"
284 284 style += "top: #{headers_height + 1}px;"
285 285 style += "left: 0px;"
286 286 style += "width: #{g_width - 1}px;"
287 287 %>
288 288 <%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %>
289 289 </div>
290 290 </td>
291 291 </tr>
292 292 </table>
293 293
294 294 <table style="width:100%">
295 295 <tr>
296 296 <td style="text-align:left;">
297 297 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
298 params.merge(@gantt.params_previous)) %>
298 params.merge(@gantt.params_previous),
299 :accesskey => accesskey(:previous)) %>
299 300 </td>
300 301 <td style="text-align:right;">
301 302 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
302 params.merge(@gantt.params_next)) %>
303 params.merge(@gantt.params_next),
304 :accesskey => accesskey(:next)) %>
303 305 </td>
304 306 </tr>
305 307 </table>
306 308
307 309 <% other_formats_links do |f| %>
308 310 <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %>
309 311 <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %>
310 312 <% end %>
311 313 <% end # query.valid? %>
312 314
313 315 <% content_for :sidebar do %>
314 316 <%= render :partial => 'issues/sidebar' %>
315 317 <% end %>
316 318
317 319 <% html_title(l(:label_gantt)) -%>
318 320
319 321 <% content_for :header_tags do %>
320 322 <%= javascript_include_tag 'raphael' %>
321 323 <%= javascript_include_tag 'gantt' %>
322 324 <% end %>
323 325
324 326 <%= javascript_tag do %>
325 327 var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>;
326 328 $(document).ready(drawGanttHandler);
327 329 $(window).resize(drawGanttHandler);
328 330 $(function() {
329 331 $("#draw_relations").change(drawGanttHandler);
330 332 $("#draw_progress_line").change(drawGanttHandler);
331 333 });
332 334 <% end %>
@@ -1,158 +1,160
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%= issue_heading(@issue) %></h2>
4 4
5 5 <div class="<%= @issue.css_classes %> details">
6 6 <% if @prev_issue_id || @next_issue_id %>
7 7 <div class="next-prev-links contextual">
8 8 <%= link_to_if @prev_issue_id,
9 9 "\xc2\xab #{l(:label_previous)}",
10 10 (@prev_issue_id ? issue_path(@prev_issue_id) : nil),
11 :title => "##{@prev_issue_id}" %> |
11 :title => "##{@prev_issue_id}",
12 :accesskey => accesskey(:previous) %> |
12 13 <% if @issue_position && @issue_count %>
13 14 <span class="position"><%= l(:label_item_position, :position => @issue_position, :count => @issue_count) %></span> |
14 15 <% end %>
15 16 <%= link_to_if @next_issue_id,
16 17 "#{l(:label_next)} \xc2\xbb",
17 18 (@next_issue_id ? issue_path(@next_issue_id) : nil),
18 :title => "##{@next_issue_id}" %>
19 :title => "##{@next_issue_id}",
20 :accesskey => accesskey(:next) %>
19 21 </div>
20 22 <% end %>
21 23
22 24 <%= avatar(@issue.author, :size => "50") %>
23 25
24 26 <div class="subject">
25 27 <%= render_issue_subject_with_tree(@issue) %>
26 28 </div>
27 29 <p class="author">
28 30 <%= authoring @issue.created_on, @issue.author %>.
29 31 <% if @issue.created_on != @issue.updated_on %>
30 32 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
31 33 <% end %>
32 34 </p>
33 35
34 36 <table class="attributes">
35 37 <%= issue_fields_rows do |rows|
36 38 rows.left l(:field_status), h(@issue.status.name), :class => 'status'
37 39 rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
38 40
39 41 unless @issue.disabled_core_fields.include?('assigned_to_id')
40 42 rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
41 43 end
42 44 unless @issue.disabled_core_fields.include?('category_id')
43 45 rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
44 46 end
45 47 unless @issue.disabled_core_fields.include?('fixed_version_id')
46 48 rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
47 49 end
48 50
49 51 unless @issue.disabled_core_fields.include?('start_date')
50 52 rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
51 53 end
52 54 unless @issue.disabled_core_fields.include?('due_date')
53 55 rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
54 56 end
55 57 unless @issue.disabled_core_fields.include?('done_ratio')
56 58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
57 59 end
58 60 unless @issue.disabled_core_fields.include?('estimated_hours')
59 61 unless @issue.estimated_hours.nil?
60 62 rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
61 63 end
62 64 end
63 65 if User.current.allowed_to?(:view_time_entries, @project)
64 66 rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
65 67 end
66 68 end %>
67 69 <%= render_custom_fields_rows(@issue) %>
68 70 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
69 71 </table>
70 72
71 73 <% if @issue.description? || @issue.attachments.any? -%>
72 74 <hr />
73 75 <% if @issue.description? %>
74 76 <div class="description">
75 77 <div class="contextual">
76 78 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
77 79 </div>
78 80
79 81 <p><strong><%=l(:field_description)%></strong></p>
80 82 <div class="wiki">
81 83 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
82 84 </div>
83 85 </div>
84 86 <% end %>
85 87 <%= link_to_attachments @issue, :thumbnails => true %>
86 88 <% end -%>
87 89
88 90 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
89 91
90 92 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
91 93 <hr />
92 94 <div id="issue_tree">
93 95 <div class="contextual">
94 96 <%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
95 97 </div>
96 98 <p><strong><%=l(:label_subtask_plural)%></strong></p>
97 99 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
98 100 </div>
99 101 <% end %>
100 102
101 103 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
102 104 <hr />
103 105 <div id="relations">
104 106 <%= render :partial => 'relations' %>
105 107 </div>
106 108 <% end %>
107 109
108 110 </div>
109 111
110 112 <% if @changesets.present? %>
111 113 <div id="issue-changesets">
112 114 <h3><%=l(:label_associated_revisions)%></h3>
113 115 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
114 116 </div>
115 117 <% end %>
116 118
117 119 <% if @journals.present? %>
118 120 <div id="history">
119 121 <h3><%=l(:label_history)%></h3>
120 122 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
121 123 </div>
122 124 <% end %>
123 125
124 126
125 127 <div style="clear: both;"></div>
126 128 <%= render :partial => 'action_menu' %>
127 129
128 130 <div style="clear: both;"></div>
129 131 <% if @issue.editable? %>
130 132 <div id="update" style="display:none;">
131 133 <h3><%= l(:button_edit) %></h3>
132 134 <%= render :partial => 'edit' %>
133 135 </div>
134 136 <% end %>
135 137
136 138 <% other_formats_links do |f| %>
137 139 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
138 140 <%= f.link_to 'PDF' %>
139 141 <% end %>
140 142
141 143 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
142 144
143 145 <% content_for :sidebar do %>
144 146 <%= render :partial => 'issues/sidebar' %>
145 147
146 148 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
147 149 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
148 150 <div id="watchers">
149 151 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
150 152 </div>
151 153 <% end %>
152 154 <% end %>
153 155
154 156 <% content_for :header_tags do %>
155 157 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
156 158 <% end %>
157 159
158 160 <%= context_menu issues_context_menu_path %>
@@ -1,101 +1,103
1 1 <div class="contextual">
2 2 &#171;
3 3 <% unless @changeset.previous.nil? -%>
4 <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %>
4 <%= link_to_revision(@changeset.previous, @repository,
5 :text => l(:label_previous), :accesskey => accesskey(:previous)) %>
5 6 <% else -%>
6 7 <%= l(:label_previous) %>
7 8 <% end -%>
8 9 |
9 10 <% unless @changeset.next.nil? -%>
10 <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %>
11 <%= link_to_revision(@changeset.next, @repository,
12 :text => l(:label_next), :accesskey => accesskey(:next)) %>
11 13 <% else -%>
12 14 <%= l(:label_next) %>
13 15 <% end -%>
14 16 &#187;&nbsp;
15 17
16 18 <%= form_tag({:controller => 'repositories',
17 19 :action => 'revision',
18 20 :id => @project,
19 21 :repository_id => @repository.identifier_param,
20 22 :rev => nil},
21 23 :method => :get) do %>
22 24 <%= text_field_tag 'rev', @rev, :size => 8 %>
23 25 <%= submit_tag 'OK', :name => nil %>
24 26 <% end %>
25 27 </div>
26 28
27 29 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
28 30
29 31 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
30 32 <table class="revision-info">
31 33 <% if @changeset.scmid.present? %>
32 34 <tr>
33 35 <td>ID</td><td><%= h(@changeset.scmid) %></td>
34 36 </tr>
35 37 <% end %>
36 38 <% if @changeset.parents.present? %>
37 39 <tr>
38 40 <td><%= l(:label_parent_revision) %></td>
39 41 <td>
40 42 <%= @changeset.parents.collect{
41 43 |p| link_to_revision(p, @repository, :text => format_revision(p))
42 44 }.join(", ").html_safe %>
43 45 </td>
44 46 </tr>
45 47 <% end %>
46 48 <% if @changeset.children.present? %>
47 49 <tr>
48 50 <td><%= l(:label_child_revision) %></td>
49 51 <td>
50 52 <%= @changeset.children.collect{
51 53 |p| link_to_revision(p, @repository, :text => format_revision(p))
52 54 }.join(", ").html_safe %>
53 55 </td>
54 56 </tr>
55 57 <% end %>
56 58 </table>
57 59 <% end %>
58 60
59 61 <p>
60 62 <span class="author">
61 63 <%= authoring(@changeset.committed_on, @changeset.author) %>
62 64 </span>
63 65 </p>
64 66
65 67 <%= textilizable @changeset.comments %>
66 68
67 69 <% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
68 70 <%= render :partial => 'related_issues' %>
69 71 <% end %>
70 72
71 73 <% if User.current.allowed_to?(:browse_repository, @project) %>
72 74 <h3><%= l(:label_attachment_plural) %></h3>
73 75 <ul id="changes-legend">
74 76 <li class="change change-A"><%= l(:label_added) %></li>
75 77 <li class="change change-M"><%= l(:label_modified) %></li>
76 78 <li class="change change-C"><%= l(:label_copied) %></li>
77 79 <li class="change change-R"><%= l(:label_renamed) %></li>
78 80 <li class="change change-D"><%= l(:label_deleted) %></li>
79 81 </ul>
80 82
81 83 <p><%= link_to(l(:label_view_diff),
82 84 :action => 'diff',
83 85 :id => @project,
84 86 :repository_id => @repository.identifier_param,
85 87 :path => "",
86 88 :rev => @changeset.identifier) if @changeset.filechanges.any? %></p>
87 89
88 90 <div class="changeset-changes">
89 91 <%= render_changeset_changes %>
90 92 </div>
91 93 <% end %>
92 94
93 95 <% content_for :header_tags do %>
94 96 <%= stylesheet_link_tag "scm" %>
95 97 <% end %>
96 98
97 99 <%
98 100 title = "#{l(:label_revision)} #{format_revision(@changeset)}"
99 101 title << " - #{@changeset.comments.truncate(80)}"
100 102 html_title(title)
101 103 -%>
@@ -1,31 +1,33
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module AccessKeys
20 20 ACCESSKEYS = {:edit => 'e',
21 21 :preview => 'r',
22 22 :quick_search => 'f',
23 23 :search => '4',
24 :new_issue => '7'
24 :new_issue => '7',
25 :previous => 'p',
26 :next => 'n'
25 27 }.freeze unless const_defined?(:ACCESSKEYS)
26 28
27 29 def self.key_for(action)
28 30 ACCESSKEYS[action]
29 31 end
30 32 end
31 33 end
@@ -1,244 +1,246
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 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 module Redmine
21 21 module Pagination
22 22 class Paginator
23 23 attr_reader :item_count, :per_page, :page, :page_param
24 24
25 25 def initialize(*args)
26 26 if args.first.is_a?(ActionController::Base)
27 27 args.shift
28 28 ActiveSupport::Deprecation.warn "Paginator no longer takes a controller instance as the first argument. Remove it from #new arguments."
29 29 end
30 30 item_count, per_page, page, page_param = *args
31 31
32 32 @item_count = item_count
33 33 @per_page = per_page
34 34 page = (page || 1).to_i
35 35 if page < 1
36 36 page = 1
37 37 end
38 38 @page = page
39 39 @page_param = page_param || :page
40 40 end
41 41
42 42 def offset
43 43 (page - 1) * per_page
44 44 end
45 45
46 46 def first_page
47 47 if item_count > 0
48 48 1
49 49 end
50 50 end
51 51
52 52 def previous_page
53 53 if page > 1
54 54 page - 1
55 55 end
56 56 end
57 57
58 58 def next_page
59 59 if last_item < item_count
60 60 page + 1
61 61 end
62 62 end
63 63
64 64 def last_page
65 65 if item_count > 0
66 66 (item_count - 1) / per_page + 1
67 67 end
68 68 end
69 69
70 70 def first_item
71 71 item_count == 0 ? 0 : (offset + 1)
72 72 end
73 73
74 74 def last_item
75 75 l = first_item + per_page - 1
76 76 l > item_count ? item_count : l
77 77 end
78 78
79 79 def linked_pages
80 80 pages = []
81 81 if item_count > 0
82 82 pages += [first_page, page, last_page]
83 83 pages += ((page-2)..(page+2)).to_a.select {|p| p > first_page && p < last_page}
84 84 end
85 85 pages = pages.compact.uniq.sort
86 86 if pages.size > 1
87 87 pages
88 88 else
89 89 []
90 90 end
91 91 end
92 92
93 93 def items_per_page
94 94 ActiveSupport::Deprecation.warn "Paginator#items_per_page will be removed. Use #per_page instead."
95 95 per_page
96 96 end
97 97
98 98 def current
99 99 ActiveSupport::Deprecation.warn "Paginator#current will be removed. Use .offset instead of .current.offset."
100 100 self
101 101 end
102 102 end
103 103
104 104 # Paginates the given scope or model. Returns a Paginator instance and
105 105 # the collection of objects for the current page.
106 106 #
107 107 # Options:
108 108 # :parameter name of the page parameter
109 109 #
110 110 # Examples:
111 111 # @user_pages, @users = paginate User.where(:status => 1)
112 112 #
113 113 def paginate(scope, options={})
114 114 options = options.dup
115 115 finder_options = options.extract!(
116 116 :conditions,
117 117 :order,
118 118 :joins,
119 119 :include,
120 120 :select
121 121 )
122 122 if scope.is_a?(Symbol) || finder_options.values.compact.any?
123 123 return deprecated_paginate(scope, finder_options, options)
124 124 end
125 125
126 126 paginator = paginator(scope.count, options)
127 127 collection = scope.limit(paginator.per_page).offset(paginator.offset).to_a
128 128
129 129 return paginator, collection
130 130 end
131 131
132 132 def deprecated_paginate(arg, finder_options, options={})
133 133 ActiveSupport::Deprecation.warn "#paginate with a Symbol and/or find options is depreceted and will be removed. Use a scope instead."
134 134 klass = arg.is_a?(Symbol) ? arg.to_s.classify.constantize : arg
135 135 scope = klass.scoped(finder_options)
136 136 paginate(scope, options)
137 137 end
138 138
139 139 def paginator(item_count, options={})
140 140 options.assert_valid_keys :parameter, :per_page
141 141
142 142 page_param = options[:parameter] || :page
143 143 page = (params[page_param] || 1).to_i
144 144 per_page = options[:per_page] || per_page_option
145 145 Paginator.new(item_count, per_page, page, page_param)
146 146 end
147 147
148 148 module Helper
149 149 include Redmine::I18n
150 150
151 151 # Renders the pagination links for the given paginator.
152 152 #
153 153 # Options:
154 154 # :per_page_links if set to false, the "Per page" links are not rendered
155 155 #
156 156 def pagination_links_full(*args)
157 157 pagination_links_each(*args) do |text, parameters, options|
158 158 if block_given?
159 159 yield text, parameters, options
160 160 else
161 161 link_to text, params.merge(parameters), options
162 162 end
163 163 end
164 164 end
165 165
166 166 # Yields the given block with the text and parameters
167 167 # for each pagination link and returns a string that represents the links
168 168 def pagination_links_each(paginator, count=nil, options={}, &block)
169 169 options.assert_valid_keys :per_page_links
170 170
171 171 per_page_links = options.delete(:per_page_links)
172 172 per_page_links = false if count.nil?
173 173 page_param = paginator.page_param
174 174
175 175 html = ''
176 176 if paginator.previous_page
177 177 # \xc2\xab(utf-8) = &#171;
178 178 text = "\xc2\xab " + l(:label_previous)
179 html << yield(text, {page_param => paginator.previous_page}, :class => 'previous') + ' '
179 html << yield(text, {page_param => paginator.previous_page},
180 :class => 'previous', :accesskey => accesskey(:previous)) + ' '
180 181 end
181 182
182 183 previous = nil
183 184 paginator.linked_pages.each do |page|
184 185 if previous && previous != page - 1
185 186 html << content_tag('span', '...', :class => 'spacer') + ' '
186 187 end
187 188 if page == paginator.page
188 189 html << content_tag('span', page.to_s, :class => 'current page')
189 190 else
190 191 html << yield(page.to_s, {page_param => page}, :class => 'page')
191 192 end
192 193 html << ' '
193 194 previous = page
194 195 end
195 196
196 197 if paginator.next_page
197 198 # \xc2\xbb(utf-8) = &#187;
198 199 text = l(:label_next) + " \xc2\xbb"
199 html << yield(text, {page_param => paginator.next_page}, :class => 'next') + ' '
200 html << yield(text, {page_param => paginator.next_page},
201 :class => 'next', :accesskey => accesskey(:next)) + ' '
200 202 end
201 203
202 204 html << content_tag('span', "(#{paginator.first_item}-#{paginator.last_item}/#{paginator.item_count})", :class => 'items') + ' '
203 205
204 206 if per_page_links != false && links = per_page_links(paginator, &block)
205 207 html << content_tag('span', links.to_s, :class => 'per-page')
206 208 end
207 209
208 210 html.html_safe
209 211 end
210 212
211 213 # Renders the "Per page" links.
212 214 def per_page_links(paginator, &block)
213 215 values = per_page_options(paginator.per_page, paginator.item_count)
214 216 if values.any?
215 217 links = values.collect do |n|
216 218 if n == paginator.per_page
217 219 content_tag('span', n.to_s)
218 220 else
219 221 yield(n, :per_page => n, paginator.page_param => nil)
220 222 end
221 223 end
222 224 l(:label_display_per_page, links.join(', ')).html_safe
223 225 end
224 226 end
225 227
226 228 def per_page_options(selected=nil, item_count=nil)
227 229 options = Setting.per_page_options_array
228 230 if item_count && options.any?
229 231 if item_count > options.first
230 232 max = options.detect {|value| value >= item_count} || item_count
231 233 else
232 234 max = item_count
233 235 end
234 236 options = options.select {|value| value <= max || value == selected}
235 237 end
236 238 if options.empty? || (options.size == 1 && options.first == selected)
237 239 []
238 240 else
239 241 options
240 242 end
241 243 end
242 244 end
243 245 end
244 246 end
General Comments 0
You need to be logged in to leave comments. Login now