##// END OF EJS Templates
Adds support for saturday as the first week day (#7097)....
Jean-Philippe Lang -
r5108:78af4f429fc8
parent child
Show More
@@ -1,947 +1,949
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 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :title => title
86 86 s << ": #{h subject}" if subject
87 87 s = "#{h issue.project} - " + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, 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, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 108
109 109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 110 :title => l(:label_revision_id, format_revision(revision)))
111 111 end
112 112
113 113 # Generates a link to a message
114 114 def link_to_message(message, options={}, html_options = nil)
115 115 link_to(
116 116 h(truncate(message.subject, :length => 60)),
117 117 { :controller => 'messages', :action => 'show',
118 118 :board_id => message.board_id,
119 119 :id => message.root,
120 120 :r => (message.parent_id && message.id),
121 121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 122 }.merge(options),
123 123 html_options
124 124 )
125 125 end
126 126
127 127 # Generates a link to a project if active
128 128 # Examples:
129 129 #
130 130 # link_to_project(project) # => link to the specified project overview
131 131 # link_to_project(project, :action=>'settings') # => link to project settings
132 132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 134 #
135 135 def link_to_project(project, options={}, html_options = nil)
136 136 if project.active?
137 137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 138 link_to(h(project), url, html_options)
139 139 else
140 140 h(project)
141 141 end
142 142 end
143 143
144 144 def toggle_link(name, id, options={})
145 145 onclick = "Element.toggle('#{id}'); "
146 146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 147 onclick << "return false;"
148 148 link_to(name, "#", :onclick => onclick)
149 149 end
150 150
151 151 def image_to_function(name, function, html_options = {})
152 152 html_options.symbolize_keys!
153 153 tag(:input, html_options.merge({
154 154 :type => "image", :src => image_path(name),
155 155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 156 }))
157 157 end
158 158
159 159 def prompt_to_remote(name, text, param, url, html_options = {})
160 160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 161 link_to name, {}, html_options
162 162 end
163 163
164 164 def format_activity_title(text)
165 165 h(truncate_single_line(text, :length => 100))
166 166 end
167 167
168 168 def format_activity_day(date)
169 169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 170 end
171 171
172 172 def format_activity_description(text)
173 173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 174 end
175 175
176 176 def format_version_name(version)
177 177 if version.project == @project
178 178 h(version)
179 179 else
180 180 h("#{version.project} - #{version}")
181 181 end
182 182 end
183 183
184 184 def due_date_distance_in_words(date)
185 185 if date
186 186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 187 end
188 188 end
189 189
190 190 def render_page_hierarchy(pages, node=nil, options={})
191 191 content = ''
192 192 if pages[node]
193 193 content << "<ul class=\"pages-hierarchy\">\n"
194 194 pages[node].each do |page|
195 195 content << "<li>"
196 196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
199 199 content << "</li>\n"
200 200 end
201 201 content << "</ul>\n"
202 202 end
203 203 content
204 204 end
205 205
206 206 # Renders flash messages
207 207 def render_flash_messages
208 208 s = ''
209 209 flash.each do |k,v|
210 210 s << content_tag('div', v, :class => "flash #{k}")
211 211 end
212 212 s
213 213 end
214 214
215 215 # Renders tabs and their content
216 216 def render_tabs(tabs)
217 217 if tabs.any?
218 218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 219 else
220 220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 221 end
222 222 end
223 223
224 224 # Renders the project quick-jump box
225 225 def render_project_jump_box
226 226 projects = User.current.memberships.collect(&:project).compact.uniq
227 227 if projects.any?
228 228 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
229 229 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
230 230 '<option value="" disabled="disabled">---</option>'
231 231 s << project_tree_options_for_select(projects, :selected => @project) do |p|
232 232 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
233 233 end
234 234 s << '</select>'
235 235 s
236 236 end
237 237 end
238 238
239 239 def project_tree_options_for_select(projects, options = {})
240 240 s = ''
241 241 project_tree(projects) do |project, level|
242 242 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
243 243 tag_options = {:value => project.id}
244 244 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
245 245 tag_options[:selected] = 'selected'
246 246 else
247 247 tag_options[:selected] = nil
248 248 end
249 249 tag_options.merge!(yield(project)) if block_given?
250 250 s << content_tag('option', name_prefix + h(project), tag_options)
251 251 end
252 252 s
253 253 end
254 254
255 255 # Yields the given block for each project with its level in the tree
256 256 #
257 257 # Wrapper for Project#project_tree
258 258 def project_tree(projects, &block)
259 259 Project.project_tree(projects, &block)
260 260 end
261 261
262 262 def project_nested_ul(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 projects.sort_by(&:lft).each do |project|
267 267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 268 s << "<ul>\n"
269 269 else
270 270 ancestors.pop
271 271 s << "</li>"
272 272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 273 ancestors.pop
274 274 s << "</ul></li>\n"
275 275 end
276 276 end
277 277 s << "<li>"
278 278 s << yield(project).to_s
279 279 ancestors << project
280 280 end
281 281 s << ("</li></ul>\n" * ancestors.size)
282 282 end
283 283 s
284 284 end
285 285
286 286 def principals_check_box_tags(name, principals)
287 287 s = ''
288 288 principals.sort.each do |principal|
289 289 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
290 290 end
291 291 s
292 292 end
293 293
294 294 # Truncates and returns the string as a single line
295 295 def truncate_single_line(string, *args)
296 296 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
297 297 end
298 298
299 299 # Truncates at line break after 250 characters or options[:length]
300 300 def truncate_lines(string, options={})
301 301 length = options[:length] || 250
302 302 if string.to_s =~ /\A(.{#{length}}.*?)$/m
303 303 "#{$1}..."
304 304 else
305 305 string
306 306 end
307 307 end
308 308
309 309 def html_hours(text)
310 310 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
311 311 end
312 312
313 313 def authoring(created, author, options={})
314 314 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
315 315 end
316 316
317 317 def time_tag(time)
318 318 text = distance_of_time_in_words(Time.now, time)
319 319 if @project
320 320 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
321 321 else
322 322 content_tag('acronym', text, :title => format_time(time))
323 323 end
324 324 end
325 325
326 326 def syntax_highlight(name, content)
327 327 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
328 328 end
329 329
330 330 def to_path_param(path)
331 331 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
332 332 end
333 333
334 334 def pagination_links_full(paginator, count=nil, options={})
335 335 page_param = options.delete(:page_param) || :page
336 336 per_page_links = options.delete(:per_page_links)
337 337 url_param = params.dup
338 338 # don't reuse query params if filters are present
339 339 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
340 340
341 341 html = ''
342 342 if paginator.current.previous
343 343 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
344 344 end
345 345
346 346 html << (pagination_links_each(paginator, options) do |n|
347 347 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
348 348 end || '')
349 349
350 350 if paginator.current.next
351 351 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
352 352 end
353 353
354 354 unless count.nil?
355 355 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
356 356 if per_page_links != false && links = per_page_links(paginator.items_per_page)
357 357 html << " | #{links}"
358 358 end
359 359 end
360 360
361 361 html
362 362 end
363 363
364 364 def per_page_links(selected=nil)
365 365 url_param = params.dup
366 366 url_param.clear if url_param.has_key?(:set_filter)
367 367
368 368 links = Setting.per_page_options_array.collect do |n|
369 369 n == selected ? n : link_to_remote(n, {:update => "content",
370 370 :url => params.dup.merge(:per_page => n),
371 371 :method => :get},
372 372 {:href => url_for(url_param.merge(:per_page => n))})
373 373 end
374 374 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
375 375 end
376 376
377 377 def reorder_links(name, url)
378 378 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
379 379 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
380 380 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
381 381 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
382 382 end
383 383
384 384 def breadcrumb(*args)
385 385 elements = args.flatten
386 386 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
387 387 end
388 388
389 389 def other_formats_links(&block)
390 390 concat('<p class="other-formats">' + l(:label_export_to))
391 391 yield Redmine::Views::OtherFormatsBuilder.new(self)
392 392 concat('</p>')
393 393 end
394 394
395 395 def page_header_title
396 396 if @project.nil? || @project.new_record?
397 397 h(Setting.app_title)
398 398 else
399 399 b = []
400 400 ancestors = (@project.root? ? [] : @project.ancestors.visible)
401 401 if ancestors.any?
402 402 root = ancestors.shift
403 403 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
404 404 if ancestors.size > 2
405 405 b << '&#8230;'
406 406 ancestors = ancestors[-2, 2]
407 407 end
408 408 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
409 409 end
410 410 b << h(@project)
411 411 b.join(' &#187; ')
412 412 end
413 413 end
414 414
415 415 def html_title(*args)
416 416 if args.empty?
417 417 title = []
418 418 title << @project.name if @project
419 419 title += @html_title if @html_title
420 420 title << Setting.app_title
421 421 title.select {|t| !t.blank? }.join(' - ')
422 422 else
423 423 @html_title ||= []
424 424 @html_title += args
425 425 end
426 426 end
427 427
428 428 # Returns the theme, controller name, and action as css classes for the
429 429 # HTML body.
430 430 def body_css_classes
431 431 css = []
432 432 if theme = Redmine::Themes.theme(Setting.ui_theme)
433 433 css << 'theme-' + theme.name
434 434 end
435 435
436 436 css << 'controller-' + params[:controller]
437 437 css << 'action-' + params[:action]
438 438 css.join(' ')
439 439 end
440 440
441 441 def accesskey(s)
442 442 Redmine::AccessKeys.key_for s
443 443 end
444 444
445 445 # Formats text according to system settings.
446 446 # 2 ways to call this method:
447 447 # * with a String: textilizable(text, options)
448 448 # * with an object and one of its attribute: textilizable(issue, :description, options)
449 449 def textilizable(*args)
450 450 options = args.last.is_a?(Hash) ? args.pop : {}
451 451 case args.size
452 452 when 1
453 453 obj = options[:object]
454 454 text = args.shift
455 455 when 2
456 456 obj = args.shift
457 457 attr = args.shift
458 458 text = obj.send(attr).to_s
459 459 else
460 460 raise ArgumentError, 'invalid arguments to textilizable'
461 461 end
462 462 return '' if text.blank?
463 463 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
464 464 only_path = options.delete(:only_path) == false ? false : true
465 465
466 466 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
467 467
468 468 @parsed_headings = []
469 469 text = parse_non_pre_blocks(text) do |text|
470 470 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
471 471 send method_name, text, project, obj, attr, only_path, options
472 472 end
473 473 end
474 474
475 475 if @parsed_headings.any?
476 476 replace_toc(text, @parsed_headings)
477 477 end
478 478
479 479 text
480 480 end
481 481
482 482 def parse_non_pre_blocks(text)
483 483 s = StringScanner.new(text)
484 484 tags = []
485 485 parsed = ''
486 486 while !s.eos?
487 487 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
488 488 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
489 489 if tags.empty?
490 490 yield text
491 491 end
492 492 parsed << text
493 493 if tag
494 494 if closing
495 495 if tags.last == tag.downcase
496 496 tags.pop
497 497 end
498 498 else
499 499 tags << tag.downcase
500 500 end
501 501 parsed << full_tag
502 502 end
503 503 end
504 504 # Close any non closing tags
505 505 while tag = tags.pop
506 506 parsed << "</#{tag}>"
507 507 end
508 508 parsed
509 509 end
510 510
511 511 def parse_inline_attachments(text, project, obj, attr, only_path, options)
512 512 # when using an image link, try to use an attachment, if possible
513 513 if options[:attachments] || (obj && obj.respond_to?(:attachments))
514 514 attachments = nil
515 515 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
516 516 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
517 517 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
518 518 # search for the picture in attachments
519 519 if found = attachments.detect { |att| att.filename.downcase == filename }
520 520 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
521 521 desc = found.description.to_s.gsub('"', '')
522 522 if !desc.blank? && alttext.blank?
523 523 alt = " title=\"#{desc}\" alt=\"#{desc}\""
524 524 end
525 525 "src=\"#{image_url}\"#{alt}"
526 526 else
527 527 m
528 528 end
529 529 end
530 530 end
531 531 end
532 532
533 533 # Wiki links
534 534 #
535 535 # Examples:
536 536 # [[mypage]]
537 537 # [[mypage|mytext]]
538 538 # wiki links can refer other project wikis, using project name or identifier:
539 539 # [[project:]] -> wiki starting page
540 540 # [[project:|mytext]]
541 541 # [[project:mypage]]
542 542 # [[project:mypage|mytext]]
543 543 def parse_wiki_links(text, project, obj, attr, only_path, options)
544 544 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
545 545 link_project = project
546 546 esc, all, page, title = $1, $2, $3, $5
547 547 if esc.nil?
548 548 if page =~ /^([^\:]+)\:(.*)$/
549 549 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
550 550 page = $2
551 551 title ||= $1 if page.blank?
552 552 end
553 553
554 554 if link_project && link_project.wiki
555 555 # extract anchor
556 556 anchor = nil
557 557 if page =~ /^(.+?)\#(.+)$/
558 558 page, anchor = $1, $2
559 559 end
560 560 # check if page exists
561 561 wiki_page = link_project.wiki.find_page(page)
562 562 url = case options[:wiki_links]
563 563 when :local; "#{title}.html"
564 564 when :anchor; "##{title}" # used for single-file wiki export
565 565 else
566 566 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
567 567 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
568 568 end
569 569 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
570 570 else
571 571 # project or wiki doesn't exist
572 572 all
573 573 end
574 574 else
575 575 all
576 576 end
577 577 end
578 578 end
579 579
580 580 # Redmine links
581 581 #
582 582 # Examples:
583 583 # Issues:
584 584 # #52 -> Link to issue #52
585 585 # Changesets:
586 586 # r52 -> Link to revision 52
587 587 # commit:a85130f -> Link to scmid starting with a85130f
588 588 # Documents:
589 589 # document#17 -> Link to document with id 17
590 590 # document:Greetings -> Link to the document with title "Greetings"
591 591 # document:"Some document" -> Link to the document with title "Some document"
592 592 # Versions:
593 593 # version#3 -> Link to version with id 3
594 594 # version:1.0.0 -> Link to version named "1.0.0"
595 595 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
596 596 # Attachments:
597 597 # attachment:file.zip -> Link to the attachment of the current object named file.zip
598 598 # Source files:
599 599 # source:some/file -> Link to the file located at /some/file in the project's repository
600 600 # source:some/file@52 -> Link to the file's revision 52
601 601 # source:some/file#L120 -> Link to line 120 of the file
602 602 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
603 603 # export:some/file -> Force the download of the file
604 604 # Forum messages:
605 605 # message#1218 -> Link to message with id 1218
606 606 #
607 607 # Links can refer other objects from other projects, using project identifier:
608 608 # identifier:r52
609 609 # identifier:document:"Some document"
610 610 # identifier:version:1.0.0
611 611 # identifier:source:some/file
612 612 def parse_redmine_links(text, project, obj, attr, only_path, options)
613 613 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
614 614 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
615 615 link = nil
616 616 if project_identifier
617 617 project = Project.visible.find_by_identifier(project_identifier)
618 618 end
619 619 if esc.nil?
620 620 if prefix.nil? && sep == 'r'
621 621 # project.changesets.visible raises an SQL error because of a double join on repositories
622 622 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
623 623 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
624 624 :class => 'changeset',
625 625 :title => truncate_single_line(changeset.comments, :length => 100))
626 626 end
627 627 elsif sep == '#'
628 628 oid = identifier.to_i
629 629 case prefix
630 630 when nil
631 631 if issue = Issue.visible.find_by_id(oid, :include => :status)
632 632 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
633 633 :class => issue.css_classes,
634 634 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
635 635 end
636 636 when 'document'
637 637 if document = Document.visible.find_by_id(oid)
638 638 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
639 639 :class => 'document'
640 640 end
641 641 when 'version'
642 642 if version = Version.visible.find_by_id(oid)
643 643 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
644 644 :class => 'version'
645 645 end
646 646 when 'message'
647 647 if message = Message.visible.find_by_id(oid, :include => :parent)
648 648 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
649 649 end
650 650 when 'project'
651 651 if p = Project.visible.find_by_id(oid)
652 652 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
653 653 end
654 654 end
655 655 elsif sep == ':'
656 656 # removes the double quotes if any
657 657 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
658 658 case prefix
659 659 when 'document'
660 660 if project && document = project.documents.visible.find_by_title(name)
661 661 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
662 662 :class => 'document'
663 663 end
664 664 when 'version'
665 665 if project && version = project.versions.visible.find_by_name(name)
666 666 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
667 667 :class => 'version'
668 668 end
669 669 when 'commit'
670 670 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
671 671 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
672 672 :class => 'changeset',
673 673 :title => truncate_single_line(changeset.comments, :length => 100)
674 674 end
675 675 when 'source', 'export'
676 676 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
677 677 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
678 678 path, rev, anchor = $1, $3, $5
679 679 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
680 680 :path => to_path_param(path),
681 681 :rev => rev,
682 682 :anchor => anchor,
683 683 :format => (prefix == 'export' ? 'raw' : nil)},
684 684 :class => (prefix == 'export' ? 'source download' : 'source')
685 685 end
686 686 when 'attachment'
687 687 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
688 688 if attachments && attachment = attachments.detect {|a| a.filename == name }
689 689 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
690 690 :class => 'attachment'
691 691 end
692 692 when 'project'
693 693 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
694 694 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
695 695 end
696 696 end
697 697 end
698 698 end
699 699 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
700 700 end
701 701 end
702 702
703 703 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
704 704
705 705 # Headings and TOC
706 706 # Adds ids and links to headings unless options[:headings] is set to false
707 707 def parse_headings(text, project, obj, attr, only_path, options)
708 708 return if options[:headings] == false
709 709
710 710 text.gsub!(HEADING_RE) do
711 711 level, attrs, content = $1.to_i, $2, $3
712 712 item = strip_tags(content).strip
713 713 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
714 714 @parsed_headings << [level, anchor, item]
715 715 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
716 716 end
717 717 end
718 718
719 719 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
720 720
721 721 # Renders the TOC with given headings
722 722 def replace_toc(text, headings)
723 723 text.gsub!(TOC_RE) do
724 724 if headings.empty?
725 725 ''
726 726 else
727 727 div_class = 'toc'
728 728 div_class << ' right' if $1 == '>'
729 729 div_class << ' left' if $1 == '<'
730 730 out = "<ul class=\"#{div_class}\"><li>"
731 731 root = headings.map(&:first).min
732 732 current = root
733 733 started = false
734 734 headings.each do |level, anchor, item|
735 735 if level > current
736 736 out << '<ul><li>' * (level - current)
737 737 elsif level < current
738 738 out << "</li></ul>\n" * (current - level) + "</li><li>"
739 739 elsif started
740 740 out << '</li><li>'
741 741 end
742 742 out << "<a href=\"##{anchor}\">#{item}</a>"
743 743 current = level
744 744 started = true
745 745 end
746 746 out << '</li></ul>' * (current - root)
747 747 out << '</li></ul>'
748 748 end
749 749 end
750 750 end
751 751
752 752 # Same as Rails' simple_format helper without using paragraphs
753 753 def simple_format_without_paragraph(text)
754 754 text.to_s.
755 755 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
756 756 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
757 757 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
758 758 end
759 759
760 760 def lang_options_for_select(blank=true)
761 761 (blank ? [["(auto)", ""]] : []) +
762 762 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
763 763 end
764 764
765 765 def label_tag_for(name, option_tags = nil, options = {})
766 766 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
767 767 content_tag("label", label_text)
768 768 end
769 769
770 770 def labelled_tabular_form_for(name, object, options, &proc)
771 771 options[:html] ||= {}
772 772 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
773 773 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
774 774 end
775 775
776 776 def back_url_hidden_field_tag
777 777 back_url = params[:back_url] || request.env['HTTP_REFERER']
778 778 back_url = CGI.unescape(back_url.to_s)
779 779 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
780 780 end
781 781
782 782 def check_all_links(form_name)
783 783 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
784 784 " | " +
785 785 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
786 786 end
787 787
788 788 def progress_bar(pcts, options={})
789 789 pcts = [pcts, pcts] unless pcts.is_a?(Array)
790 790 pcts = pcts.collect(&:round)
791 791 pcts[1] = pcts[1] - pcts[0]
792 792 pcts << (100 - pcts[1] - pcts[0])
793 793 width = options[:width] || '100px;'
794 794 legend = options[:legend] || ''
795 795 content_tag('table',
796 796 content_tag('tr',
797 797 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
798 798 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
799 799 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
800 800 ), :class => 'progress', :style => "width: #{width};") +
801 801 content_tag('p', legend, :class => 'pourcent')
802 802 end
803 803
804 804 def checked_image(checked=true)
805 805 if checked
806 806 image_tag 'toggle_check.png'
807 807 end
808 808 end
809 809
810 810 def context_menu(url)
811 811 unless @context_menu_included
812 812 content_for :header_tags do
813 813 javascript_include_tag('context_menu') +
814 814 stylesheet_link_tag('context_menu')
815 815 end
816 816 if l(:direction) == 'rtl'
817 817 content_for :header_tags do
818 818 stylesheet_link_tag('context_menu_rtl')
819 819 end
820 820 end
821 821 @context_menu_included = true
822 822 end
823 823 javascript_tag "new ContextMenu('#{ url_for(url) }')"
824 824 end
825 825
826 826 def context_menu_link(name, url, options={})
827 827 options[:class] ||= ''
828 828 if options.delete(:selected)
829 829 options[:class] << ' icon-checked disabled'
830 830 options[:disabled] = true
831 831 end
832 832 if options.delete(:disabled)
833 833 options.delete(:method)
834 834 options.delete(:confirm)
835 835 options.delete(:onclick)
836 836 options[:class] << ' disabled'
837 837 url = '#'
838 838 end
839 839 link_to name, url, options
840 840 end
841 841
842 842 def calendar_for(field_id)
843 843 include_calendar_headers_tags
844 844 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
845 845 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
846 846 end
847 847
848 848 def include_calendar_headers_tags
849 849 unless @calendar_headers_tags_included
850 850 @calendar_headers_tags_included = true
851 851 content_for :header_tags do
852 852 start_of_week = case Setting.start_of_week.to_i
853 853 when 1
854 854 'Calendar._FD = 1;' # Monday
855 855 when 7
856 856 'Calendar._FD = 0;' # Sunday
857 when 6
858 'Calendar._FD = 6;' # Saturday
857 859 else
858 860 '' # use language
859 861 end
860 862
861 863 javascript_include_tag('calendar/calendar') +
862 864 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
863 865 javascript_tag(start_of_week) +
864 866 javascript_include_tag('calendar/calendar-setup') +
865 867 stylesheet_link_tag('calendar')
866 868 end
867 869 end
868 870 end
869 871
870 872 def content_for(name, content = nil, &block)
871 873 @has_content ||= {}
872 874 @has_content[name] = true
873 875 super(name, content, &block)
874 876 end
875 877
876 878 def has_content?(name)
877 879 (@has_content && @has_content[name]) || false
878 880 end
879 881
880 882 # Returns the avatar image tag for the given +user+ if avatars are enabled
881 883 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
882 884 def avatar(user, options = { })
883 885 if Setting.gravatar_enabled?
884 886 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
885 887 email = nil
886 888 if user.respond_to?(:mail)
887 889 email = user.mail
888 890 elsif user.to_s =~ %r{<(.+?)>}
889 891 email = $1
890 892 end
891 893 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
892 894 else
893 895 ''
894 896 end
895 897 end
896 898
897 899 # Returns the javascript tags that are included in the html layout head
898 900 def javascript_heads
899 901 tags = javascript_include_tag(:defaults)
900 902 unless User.current.pref.warn_on_leaving_unsaved == '0'
901 903 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
902 904 end
903 905 tags
904 906 end
905 907
906 908 def favicon
907 909 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
908 910 end
909 911
910 912 # Returns true if arg is expected in the API response
911 913 def include_in_api_response?(arg)
912 914 unless @included_in_api_response
913 915 param = params[:include]
914 916 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
915 917 @included_in_api_response.collect!(&:strip)
916 918 end
917 919 @included_in_api_response.include?(arg.to_s)
918 920 end
919 921
920 922 # Returns options or nil if nometa param or X-Redmine-Nometa header
921 923 # was set in the request
922 924 def api_meta(options)
923 925 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
924 926 # compatibility mode for activeresource clients that raise
925 927 # an error when unserializing an array with attributes
926 928 nil
927 929 else
928 930 options
929 931 end
930 932 end
931 933
932 934 private
933 935
934 936 def wiki_helper
935 937 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
936 938 extend helper
937 939 return self
938 940 end
939 941
940 942 def link_to_remote_content_update(text, url_params)
941 943 link_to_remote(text,
942 944 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
943 945 {:href => url_for(:params => url_params)}
944 946 )
945 947 end
946 948
947 949 end
@@ -1,22 +1,22
1 1 <% form_tag({:action => 'edit', :tab => 'display'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
5 5
6 6 <p><%= setting_select :default_language, lang_options_for_select(false) %></p>
7 7
8 <p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(7),'7']], :blank => :label_language_based %></p>
8 <p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(6),'6'], [day_name(7),'7']], :blank => :label_language_based %></p>
9 9
10 10 <p><%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, :blank => :label_language_based %></p>
11 11
12 12 <p><%= setting_select :time_format, Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, :blank => :label_language_based %></p>
13 13
14 14 <p><%= setting_select :user_format, @options[:user_format] %></p>
15 15
16 16 <p><%= setting_check_box :gravatar_enabled %></p>
17 17
18 18 <p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
19 19 </div>
20 20
21 21 <%= submit_tag l(:button_save) %>
22 22 <% end %>
@@ -1,83 +1,85
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Helpers
20 20
21 21 # Simple class to compute the start and end dates of a calendar
22 22 class Calendar
23 23 include Redmine::I18n
24 24 attr_reader :startdt, :enddt
25 25
26 26 def initialize(date, lang = current_language, period = :month)
27 27 @date = date
28 28 @events = []
29 29 @ending_events_by_days = {}
30 30 @starting_events_by_days = {}
31 31 set_language_if_valid lang
32 32 case period
33 33 when :month
34 34 @startdt = Date.civil(date.year, date.month, 1)
35 35 @enddt = (@startdt >> 1)-1
36 36 # starts from the first day of the week
37 37 @startdt = @startdt - (@startdt.cwday - first_wday)%7
38 38 # ends on the last day of the week
39 39 @enddt = @enddt + (last_wday - @enddt.cwday)%7
40 40 when :week
41 41 @startdt = date - (date.cwday - first_wday)%7
42 42 @enddt = date + (last_wday - date.cwday)%7
43 43 else
44 44 raise 'Invalid period'
45 45 end
46 46 end
47 47
48 48 # Sets calendar events
49 49 def events=(events)
50 50 @events = events
51 51 @ending_events_by_days = @events.group_by {|event| event.due_date}
52 52 @starting_events_by_days = @events.group_by {|event| event.start_date}
53 53 end
54 54
55 55 # Returns events for the given day
56 56 def events_on(day)
57 57 ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
58 58 end
59 59
60 60 # Calendar current month
61 61 def month
62 62 @date.month
63 63 end
64 64
65 65 # Return the first day of week
66 66 # 1 = Monday ... 7 = Sunday
67 67 def first_wday
68 68 case Setting.start_of_week.to_i
69 69 when 1
70 70 @first_dow ||= (1 - 1)%7 + 1
71 when 6
72 @first_dow ||= (6 - 1)%7 + 1
71 73 when 7
72 74 @first_dow ||= (7 - 1)%7 + 1
73 75 else
74 76 @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
75 77 end
76 78 end
77 79
78 80 def last_wday
79 81 @last_dow ||= (first_wday + 5)%7 + 1
80 82 end
81 83 end
82 84 end
83 85 end
@@ -1,43 +1,63
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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
20 20 class CalendarTest < ActiveSupport::TestCase
21 21
22 22 def test_monthly
23 23 c = Redmine::Helpers::Calendar.new(Date.today, :fr, :month)
24 24 assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday]
25 25
26 26 c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :month)
27 27 assert_equal ['2007-06-25'.to_date, '2007-08-05'.to_date], [c.startdt, c.enddt]
28 28
29 29 c = Redmine::Helpers::Calendar.new(Date.today, :en, :month)
30 30 assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday]
31 31 end
32 32
33 33 def test_weekly
34 34 c = Redmine::Helpers::Calendar.new(Date.today, :fr, :week)
35 35 assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday]
36 36
37 37 c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :week)
38 38 assert_equal ['2007-07-09'.to_date, '2007-07-15'.to_date], [c.startdt, c.enddt]
39 39
40 40 c = Redmine::Helpers::Calendar.new(Date.today, :en, :week)
41 41 assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday]
42 42 end
43
44 def test_monthly_start_day
45 [1, 6, 7].each do |day|
46 with_settings :start_of_week => day do
47 c = Redmine::Helpers::Calendar.new(Date.today, :en, :month)
48 assert_equal day , c.startdt.cwday
49 assert_equal (day + 5) % 7, c.enddt.cwday
50 end
51 end
52 end
53
54 def test_weekly_start_day
55 [1, 6, 7].each do |day|
56 with_settings :start_of_week => day do
57 c = Redmine::Helpers::Calendar.new(Date.today, :en, :week)
58 assert_equal day, c.startdt.cwday
59 assert_equal (day + 5) % 7 + 1, c.enddt.cwday
60 end
61 end
62 end
43 63 end
General Comments 0
You need to be logged in to leave comments. Login now