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