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