##// END OF EJS Templates
Adds subtasks to GET /issues/:id API (#5338)....
Jean-Philippe Lang -
r4351:915748204965
parent child
Show More
@@ -1,249 +1,263
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module IssuesHelper
18 module IssuesHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def issue_list(issues, &block)
21 def issue_list(issues, &block)
22 ancestors = []
22 ancestors = []
23 issues.each do |issue|
23 issues.each do |issue|
24 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
24 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
25 ancestors.pop
25 ancestors.pop
26 end
26 end
27 yield issue, ancestors.size
27 yield issue, ancestors.size
28 ancestors << issue unless issue.leaf?
28 ancestors << issue unless issue.leaf?
29 end
29 end
30 end
30 end
31
31
32 # Renders a HTML/CSS tooltip
32 # Renders a HTML/CSS tooltip
33 #
33 #
34 # To use, a trigger div is needed. This is a div with the class of "tooltip"
34 # To use, a trigger div is needed. This is a div with the class of "tooltip"
35 # that contains this method wrapped in a span with the class of "tip"
35 # that contains this method wrapped in a span with the class of "tip"
36 #
36 #
37 # <div class="tooltip"><%= link_to_issue(issue) %>
37 # <div class="tooltip"><%= link_to_issue(issue) %>
38 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
38 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
39 # </div>
39 # </div>
40 #
40 #
41 def render_issue_tooltip(issue)
41 def render_issue_tooltip(issue)
42 @cached_label_status ||= l(:field_status)
42 @cached_label_status ||= l(:field_status)
43 @cached_label_start_date ||= l(:field_start_date)
43 @cached_label_start_date ||= l(:field_start_date)
44 @cached_label_due_date ||= l(:field_due_date)
44 @cached_label_due_date ||= l(:field_due_date)
45 @cached_label_assigned_to ||= l(:field_assigned_to)
45 @cached_label_assigned_to ||= l(:field_assigned_to)
46 @cached_label_priority ||= l(:field_priority)
46 @cached_label_priority ||= l(:field_priority)
47 @cached_label_project ||= l(:field_project)
47 @cached_label_project ||= l(:field_project)
48
48
49 link_to_issue(issue) + "<br /><br />" +
49 link_to_issue(issue) + "<br /><br />" +
50 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
50 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
51 "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
51 "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
52 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
52 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
53 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
53 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
54 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
54 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
55 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
55 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
56 end
56 end
57
57
58 def render_issue_subject_with_tree(issue)
58 def render_issue_subject_with_tree(issue)
59 s = ''
59 s = ''
60 issue.ancestors.each do |ancestor|
60 issue.ancestors.each do |ancestor|
61 s << '<div>' + content_tag('p', link_to_issue(ancestor))
61 s << '<div>' + content_tag('p', link_to_issue(ancestor))
62 end
62 end
63 s << '<div>' + content_tag('h3', h(issue.subject))
63 s << '<div>' + content_tag('h3', h(issue.subject))
64 s << '</div>' * (issue.ancestors.size + 1)
64 s << '</div>' * (issue.ancestors.size + 1)
65 s
65 s
66 end
66 end
67
67
68 def render_descendants_tree(issue)
68 def render_descendants_tree(issue)
69 s = '<form><table class="list issues">'
69 s = '<form><table class="list issues">'
70 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
70 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
71 s << content_tag('tr',
71 s << content_tag('tr',
72 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
72 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
73 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
73 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
74 content_tag('td', h(child.status)) +
74 content_tag('td', h(child.status)) +
75 content_tag('td', link_to_user(child.assigned_to)) +
75 content_tag('td', link_to_user(child.assigned_to)) +
76 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
76 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
77 :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
77 :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
78 end
78 end
79 s << '</form></table>'
79 s << '</form></table>'
80 s
80 s
81 end
81 end
82
82
83 def render_custom_fields_rows(issue)
83 def render_custom_fields_rows(issue)
84 return if issue.custom_field_values.empty?
84 return if issue.custom_field_values.empty?
85 ordered_values = []
85 ordered_values = []
86 half = (issue.custom_field_values.size / 2.0).ceil
86 half = (issue.custom_field_values.size / 2.0).ceil
87 half.times do |i|
87 half.times do |i|
88 ordered_values << issue.custom_field_values[i]
88 ordered_values << issue.custom_field_values[i]
89 ordered_values << issue.custom_field_values[i + half]
89 ordered_values << issue.custom_field_values[i + half]
90 end
90 end
91 s = "<tr>\n"
91 s = "<tr>\n"
92 n = 0
92 n = 0
93 ordered_values.compact.each do |value|
93 ordered_values.compact.each do |value|
94 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
94 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
95 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
95 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
96 n += 1
96 n += 1
97 end
97 end
98 s << "</tr>\n"
98 s << "</tr>\n"
99 s
99 s
100 end
100 end
101
101
102 def sidebar_queries
102 def sidebar_queries
103 unless @sidebar_queries
103 unless @sidebar_queries
104 # User can see public queries and his own queries
104 # User can see public queries and his own queries
105 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
105 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
106 # Project specific queries and global queries
106 # Project specific queries and global queries
107 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
107 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
108 @sidebar_queries = Query.find(:all,
108 @sidebar_queries = Query.find(:all,
109 :select => 'id, name',
109 :select => 'id, name',
110 :order => "name ASC",
110 :order => "name ASC",
111 :conditions => visible.conditions)
111 :conditions => visible.conditions)
112 end
112 end
113 @sidebar_queries
113 @sidebar_queries
114 end
114 end
115
115
116 def show_detail(detail, no_html=false)
116 def show_detail(detail, no_html=false)
117 case detail.property
117 case detail.property
118 when 'attr'
118 when 'attr'
119 field = detail.prop_key.to_s.gsub(/\_id$/, "")
119 field = detail.prop_key.to_s.gsub(/\_id$/, "")
120 label = l(("field_" + field).to_sym)
120 label = l(("field_" + field).to_sym)
121 case
121 case
122 when ['due_date', 'start_date'].include?(detail.prop_key)
122 when ['due_date', 'start_date'].include?(detail.prop_key)
123 value = format_date(detail.value.to_date) if detail.value
123 value = format_date(detail.value.to_date) if detail.value
124 old_value = format_date(detail.old_value.to_date) if detail.old_value
124 old_value = format_date(detail.old_value.to_date) if detail.old_value
125
125
126 when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
126 when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
127 value = find_name_by_reflection(field, detail.value)
127 value = find_name_by_reflection(field, detail.value)
128 old_value = find_name_by_reflection(field, detail.old_value)
128 old_value = find_name_by_reflection(field, detail.old_value)
129
129
130 when detail.prop_key == 'estimated_hours'
130 when detail.prop_key == 'estimated_hours'
131 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
131 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
132 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
132 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
133
133
134 when detail.prop_key == 'parent_id'
134 when detail.prop_key == 'parent_id'
135 label = l(:field_parent_issue)
135 label = l(:field_parent_issue)
136 value = "##{detail.value}" unless detail.value.blank?
136 value = "##{detail.value}" unless detail.value.blank?
137 old_value = "##{detail.old_value}" unless detail.old_value.blank?
137 old_value = "##{detail.old_value}" unless detail.old_value.blank?
138 end
138 end
139 when 'cf'
139 when 'cf'
140 custom_field = CustomField.find_by_id(detail.prop_key)
140 custom_field = CustomField.find_by_id(detail.prop_key)
141 if custom_field
141 if custom_field
142 label = custom_field.name
142 label = custom_field.name
143 value = format_value(detail.value, custom_field.field_format) if detail.value
143 value = format_value(detail.value, custom_field.field_format) if detail.value
144 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
144 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
145 end
145 end
146 when 'attachment'
146 when 'attachment'
147 label = l(:label_attachment)
147 label = l(:label_attachment)
148 end
148 end
149 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
149 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
150
150
151 label ||= detail.prop_key
151 label ||= detail.prop_key
152 value ||= detail.value
152 value ||= detail.value
153 old_value ||= detail.old_value
153 old_value ||= detail.old_value
154
154
155 unless no_html
155 unless no_html
156 label = content_tag('strong', label)
156 label = content_tag('strong', label)
157 old_value = content_tag("i", h(old_value)) if detail.old_value
157 old_value = content_tag("i", h(old_value)) if detail.old_value
158 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
158 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
159 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
159 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
160 # Link to the attachment if it has not been removed
160 # Link to the attachment if it has not been removed
161 value = link_to_attachment(a)
161 value = link_to_attachment(a)
162 else
162 else
163 value = content_tag("i", h(value)) if value
163 value = content_tag("i", h(value)) if value
164 end
164 end
165 end
165 end
166
166
167 if !detail.value.blank?
167 if !detail.value.blank?
168 case detail.property
168 case detail.property
169 when 'attr', 'cf'
169 when 'attr', 'cf'
170 if !detail.old_value.blank?
170 if !detail.old_value.blank?
171 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
171 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
172 else
172 else
173 l(:text_journal_set_to, :label => label, :value => value)
173 l(:text_journal_set_to, :label => label, :value => value)
174 end
174 end
175 when 'attachment'
175 when 'attachment'
176 l(:text_journal_added, :label => label, :value => value)
176 l(:text_journal_added, :label => label, :value => value)
177 end
177 end
178 else
178 else
179 l(:text_journal_deleted, :label => label, :old => old_value)
179 l(:text_journal_deleted, :label => label, :old => old_value)
180 end
180 end
181 end
181 end
182
182
183 # Find the name of an associated record stored in the field attribute
183 # Find the name of an associated record stored in the field attribute
184 def find_name_by_reflection(field, id)
184 def find_name_by_reflection(field, id)
185 association = Issue.reflect_on_association(field.to_sym)
185 association = Issue.reflect_on_association(field.to_sym)
186 if association
186 if association
187 record = association.class_name.constantize.find_by_id(id)
187 record = association.class_name.constantize.find_by_id(id)
188 return record.name if record
188 return record.name if record
189 end
189 end
190 end
190 end
191
191
192 # Renders issue children recursively
193 def render_api_issue_children(issue, api)
194 return if issue.leaf?
195 api.array :children do
196 issue.children.each do |child|
197 api.issue(:id => child.id) do
198 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
199 api.subject child.subject
200 render_api_issue_children(child, api)
201 end
202 end
203 end
204 end
205
192 def issues_to_csv(issues, project = nil)
206 def issues_to_csv(issues, project = nil)
193 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
207 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
194 decimal_separator = l(:general_csv_decimal_separator)
208 decimal_separator = l(:general_csv_decimal_separator)
195 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
209 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
196 # csv header fields
210 # csv header fields
197 headers = [ "#",
211 headers = [ "#",
198 l(:field_status),
212 l(:field_status),
199 l(:field_project),
213 l(:field_project),
200 l(:field_tracker),
214 l(:field_tracker),
201 l(:field_priority),
215 l(:field_priority),
202 l(:field_subject),
216 l(:field_subject),
203 l(:field_assigned_to),
217 l(:field_assigned_to),
204 l(:field_category),
218 l(:field_category),
205 l(:field_fixed_version),
219 l(:field_fixed_version),
206 l(:field_author),
220 l(:field_author),
207 l(:field_start_date),
221 l(:field_start_date),
208 l(:field_due_date),
222 l(:field_due_date),
209 l(:field_done_ratio),
223 l(:field_done_ratio),
210 l(:field_estimated_hours),
224 l(:field_estimated_hours),
211 l(:field_parent_issue),
225 l(:field_parent_issue),
212 l(:field_created_on),
226 l(:field_created_on),
213 l(:field_updated_on)
227 l(:field_updated_on)
214 ]
228 ]
215 # Export project custom fields if project is given
229 # Export project custom fields if project is given
216 # otherwise export custom fields marked as "For all projects"
230 # otherwise export custom fields marked as "For all projects"
217 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
231 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
218 custom_fields.each {|f| headers << f.name}
232 custom_fields.each {|f| headers << f.name}
219 # Description in the last column
233 # Description in the last column
220 headers << l(:field_description)
234 headers << l(:field_description)
221 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
235 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
222 # csv lines
236 # csv lines
223 issues.each do |issue|
237 issues.each do |issue|
224 fields = [issue.id,
238 fields = [issue.id,
225 issue.status.name,
239 issue.status.name,
226 issue.project.name,
240 issue.project.name,
227 issue.tracker.name,
241 issue.tracker.name,
228 issue.priority.name,
242 issue.priority.name,
229 issue.subject,
243 issue.subject,
230 issue.assigned_to,
244 issue.assigned_to,
231 issue.category,
245 issue.category,
232 issue.fixed_version,
246 issue.fixed_version,
233 issue.author.name,
247 issue.author.name,
234 format_date(issue.start_date),
248 format_date(issue.start_date),
235 format_date(issue.due_date),
249 format_date(issue.due_date),
236 issue.done_ratio,
250 issue.done_ratio,
237 issue.estimated_hours.to_s.gsub('.', decimal_separator),
251 issue.estimated_hours.to_s.gsub('.', decimal_separator),
238 issue.parent_id,
252 issue.parent_id,
239 format_time(issue.created_on),
253 format_time(issue.created_on),
240 format_time(issue.updated_on)
254 format_time(issue.updated_on)
241 ]
255 ]
242 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
256 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
243 fields << issue.description
257 fields << issue.description
244 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
258 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
245 end
259 end
246 end
260 end
247 export
261 export
248 end
262 end
249 end
263 end
@@ -1,61 +1,63
1 api.issue do
1 api.issue do
2 api.id @issue.id
2 api.id @issue.id
3 api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
3 api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
4 api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
4 api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
5 api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
5 api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
6 api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
6 api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
7 api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
7 api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
8 api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
8 api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
9 api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
9 api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
10 api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
10 api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
11 api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
11 api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
12
12
13 api.subject @issue.subject
13 api.subject @issue.subject
14 api.description @issue.description
14 api.description @issue.description
15 api.start_date @issue.start_date
15 api.start_date @issue.start_date
16 api.due_date @issue.due_date
16 api.due_date @issue.due_date
17 api.done_ratio @issue.done_ratio
17 api.done_ratio @issue.done_ratio
18 api.estimated_hours @issue.estimated_hours
18 api.estimated_hours @issue.estimated_hours
19 if User.current.allowed_to?(:view_time_entries, @project)
19 if User.current.allowed_to?(:view_time_entries, @project)
20 api.spent_hours @issue.spent_hours
20 api.spent_hours @issue.spent_hours
21 end
21 end
22
22
23 api.array :custom_fields do
23 api.array :custom_fields do
24 @issue.custom_field_values.each do |custom_value|
24 @issue.custom_field_values.each do |custom_value|
25 api.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name
25 api.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name
26 end
26 end
27 end unless @issue.custom_field_values.empty?
27 end unless @issue.custom_field_values.empty?
28
28
29 api.created_on @issue.created_on
29 api.created_on @issue.created_on
30 api.updated_on @issue.updated_on
30 api.updated_on @issue.updated_on
31
31
32 render_api_issue_children(@issue, api)
33
32 api.array :relations do
34 api.array :relations do
33 @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation|
35 @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation|
34 api.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
36 api.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
35 end
37 end
36 end
38 end
37
39
38 api.array :changesets do
40 api.array :changesets do
39 @issue.changesets.each do |changeset|
41 @issue.changesets.each do |changeset|
40 api.changeset :revision => changeset.revision do
42 api.changeset :revision => changeset.revision do
41 api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
43 api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
42 api.comments changeset.comments
44 api.comments changeset.comments
43 api.committed_on changeset.committed_on
45 api.committed_on changeset.committed_on
44 end
46 end
45 end
47 end
46 end if User.current.allowed_to?(:view_changesets, @project) && @issue.changesets.any?
48 end if User.current.allowed_to?(:view_changesets, @project) && @issue.changesets.any?
47
49
48 api.array :journals do
50 api.array :journals do
49 @issue.journals.each do |journal|
51 @issue.journals.each do |journal|
50 api.journal :id => journal.id do
52 api.journal :id => journal.id do
51 api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
53 api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
52 api.notes journal.notes
54 api.notes journal.notes
53 api.array :details do
55 api.array :details do
54 journal.details.each do |detail|
56 journal.details.each do |detail|
55 api.detail :property => detail.property, :name => detail.prop_key, :old => detail.old_value, :new => detail.value
57 api.detail :property => detail.property, :name => detail.prop_key, :old => detail.old_value, :new => detail.value
56 end
58 end
57 end
59 end
58 end
60 end
59 end
61 end
60 end unless @issue.journals.empty?
62 end unless @issue.journals.empty?
61 end
63 end
@@ -1,340 +1,394
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "#{File.dirname(__FILE__)}/../../test_helper"
18 require "#{File.dirname(__FILE__)}/../../test_helper"
19
19
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :versions,
28 :versions,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :issue_categories,
31 :issue_categories,
32 :enabled_modules,
32 :enabled_modules,
33 :enumerations,
33 :enumerations,
34 :attachments,
34 :attachments,
35 :workflows,
35 :workflows,
36 :custom_fields,
36 :custom_fields,
37 :custom_values,
37 :custom_values,
38 :custom_fields_projects,
38 :custom_fields_projects,
39 :custom_fields_trackers,
39 :custom_fields_trackers,
40 :time_entries,
40 :time_entries,
41 :journals,
41 :journals,
42 :journal_details,
42 :journal_details,
43 :queries
43 :queries
44
44
45 def setup
45 def setup
46 Setting.rest_api_enabled = '1'
46 Setting.rest_api_enabled = '1'
47 end
47 end
48
48
49 # Use a private project to make sure auth is really working and not just
49 # Use a private project to make sure auth is really working and not just
50 # only showing public issues.
50 # only showing public issues.
51 context "/index.xml" do
51 context "/index.xml" do
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 end
53 end
54
54
55 context "/index.json" do
55 context "/index.json" do
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
57 end
57 end
58
58
59 context "/index.xml with filter" do
59 context "/index.xml with filter" do
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
61
61
62 should "show only issues with the status_id" do
62 should "show only issues with the status_id" do
63 get '/issues.xml?status_id=5'
63 get '/issues.xml?status_id=5'
64 assert_tag :tag => 'issues',
64 assert_tag :tag => 'issues',
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
66 :only => { :tag => 'issue' } }
66 :only => { :tag => 'issue' } }
67 end
67 end
68 end
68 end
69
69
70 context "/index.json with filter" do
70 context "/index.json with filter" do
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
72
72
73 should "show only issues with the status_id" do
73 should "show only issues with the status_id" do
74 get '/issues.json?status_id=5'
74 get '/issues.json?status_id=5'
75
75
76 json = ActiveSupport::JSON.decode(response.body)
76 json = ActiveSupport::JSON.decode(response.body)
77 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
77 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
78 assert_equal 3, status_ids_used.length
78 assert_equal 3, status_ids_used.length
79 assert status_ids_used.all? {|id| id == 5 }
79 assert status_ids_used.all? {|id| id == 5 }
80 end
80 end
81
81
82 end
82 end
83
83
84 # Issue 6 is on a private project
84 # Issue 6 is on a private project
85 context "/issues/6.xml" do
85 context "/issues/6.xml" do
86 should_allow_api_authentication(:get, "/issues/6.xml")
86 should_allow_api_authentication(:get, "/issues/6.xml")
87 end
87 end
88
88
89 context "/issues/6.json" do
89 context "/issues/6.json" do
90 should_allow_api_authentication(:get, "/issues/6.json")
90 should_allow_api_authentication(:get, "/issues/6.json")
91 end
91 end
92
92
93 context "GET /issues/:id" do
94 context "with subtasks" do
95 setup do
96 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
97 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
98 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
99 end
100
101 context ".xml" do
102 should "display children" do
103 get '/issues/1.xml'
104
105 assert_tag :tag => 'issue',
106 :child => {
107 :tag => 'children',
108 :children => {:count => 2},
109 :child => {
110 :tag => 'issue',
111 :attributes => {:id => @c1.id.to_s},
112 :child => {
113 :tag => 'subject',
114 :content => 'child c1',
115 :sibling => {
116 :tag => 'children',
117 :children => {:count => 1},
118 :child => {
119 :tag => 'issue',
120 :attributes => {:id => @c3.id.to_s}
121 }
122 }
123 }
124 }
125 }
126 end
127
128 context ".json" do
129 should "display children" do
130 get '/issues/1.json'
131
132 json = ActiveSupport::JSON.decode(response.body)
133 assert_equal([
134 {
135 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
136 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
137 },
138 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
139 ],
140 json['issue']['children'])
141 end
142 end
143 end
144 end
145 end
146
93 context "POST /issues.xml" do
147 context "POST /issues.xml" do
94 should_allow_api_authentication(:post,
148 should_allow_api_authentication(:post,
95 '/issues.xml',
149 '/issues.xml',
96 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
150 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
97 {:success_code => :created})
151 {:success_code => :created})
98
152
99 should "create an issue with the attributes" do
153 should "create an issue with the attributes" do
100 assert_difference('Issue.count') do
154 assert_difference('Issue.count') do
101 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
155 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
102 end
156 end
103
157
104 issue = Issue.first(:order => 'id DESC')
158 issue = Issue.first(:order => 'id DESC')
105 assert_equal 1, issue.project_id
159 assert_equal 1, issue.project_id
106 assert_equal 2, issue.tracker_id
160 assert_equal 2, issue.tracker_id
107 assert_equal 3, issue.status_id
161 assert_equal 3, issue.status_id
108 assert_equal 'API test', issue.subject
162 assert_equal 'API test', issue.subject
109
163
110 assert_response :created
164 assert_response :created
111 assert_equal 'application/xml', @response.content_type
165 assert_equal 'application/xml', @response.content_type
112 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
166 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
113 end
167 end
114 end
168 end
115
169
116 context "POST /issues.xml with failure" do
170 context "POST /issues.xml with failure" do
117 should_allow_api_authentication(:post,
171 should_allow_api_authentication(:post,
118 '/issues.xml',
172 '/issues.xml',
119 {:issue => {:project_id => 1}},
173 {:issue => {:project_id => 1}},
120 {:success_code => :unprocessable_entity})
174 {:success_code => :unprocessable_entity})
121
175
122 should "have an errors tag" do
176 should "have an errors tag" do
123 assert_no_difference('Issue.count') do
177 assert_no_difference('Issue.count') do
124 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
178 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
125 end
179 end
126
180
127 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
181 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
128 end
182 end
129 end
183 end
130
184
131 context "POST /issues.json" do
185 context "POST /issues.json" do
132 should_allow_api_authentication(:post,
186 should_allow_api_authentication(:post,
133 '/issues.json',
187 '/issues.json',
134 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
188 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
135 {:success_code => :created})
189 {:success_code => :created})
136
190
137 should "create an issue with the attributes" do
191 should "create an issue with the attributes" do
138 assert_difference('Issue.count') do
192 assert_difference('Issue.count') do
139 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
193 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
140 end
194 end
141
195
142 issue = Issue.first(:order => 'id DESC')
196 issue = Issue.first(:order => 'id DESC')
143 assert_equal 1, issue.project_id
197 assert_equal 1, issue.project_id
144 assert_equal 2, issue.tracker_id
198 assert_equal 2, issue.tracker_id
145 assert_equal 3, issue.status_id
199 assert_equal 3, issue.status_id
146 assert_equal 'API test', issue.subject
200 assert_equal 'API test', issue.subject
147 end
201 end
148
202
149 end
203 end
150
204
151 context "POST /issues.json with failure" do
205 context "POST /issues.json with failure" do
152 should_allow_api_authentication(:post,
206 should_allow_api_authentication(:post,
153 '/issues.json',
207 '/issues.json',
154 {:issue => {:project_id => 1}},
208 {:issue => {:project_id => 1}},
155 {:success_code => :unprocessable_entity})
209 {:success_code => :unprocessable_entity})
156
210
157 should "have an errors element" do
211 should "have an errors element" do
158 assert_no_difference('Issue.count') do
212 assert_no_difference('Issue.count') do
159 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
213 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
160 end
214 end
161
215
162 json = ActiveSupport::JSON.decode(response.body)
216 json = ActiveSupport::JSON.decode(response.body)
163 assert json['errors'].include?(['subject', "can't be blank"])
217 assert json['errors'].include?(['subject', "can't be blank"])
164 end
218 end
165 end
219 end
166
220
167 # Issue 6 is on a private project
221 # Issue 6 is on a private project
168 context "PUT /issues/6.xml" do
222 context "PUT /issues/6.xml" do
169 setup do
223 setup do
170 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
224 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
171 @headers = { :authorization => credentials('jsmith') }
225 @headers = { :authorization => credentials('jsmith') }
172 end
226 end
173
227
174 should_allow_api_authentication(:put,
228 should_allow_api_authentication(:put,
175 '/issues/6.xml',
229 '/issues/6.xml',
176 {:issue => {:subject => 'API update', :notes => 'A new note'}},
230 {:issue => {:subject => 'API update', :notes => 'A new note'}},
177 {:success_code => :ok})
231 {:success_code => :ok})
178
232
179 should "not create a new issue" do
233 should "not create a new issue" do
180 assert_no_difference('Issue.count') do
234 assert_no_difference('Issue.count') do
181 put '/issues/6.xml', @parameters, @headers
235 put '/issues/6.xml', @parameters, @headers
182 end
236 end
183 end
237 end
184
238
185 should "create a new journal" do
239 should "create a new journal" do
186 assert_difference('Journal.count') do
240 assert_difference('Journal.count') do
187 put '/issues/6.xml', @parameters, @headers
241 put '/issues/6.xml', @parameters, @headers
188 end
242 end
189 end
243 end
190
244
191 should "add the note to the journal" do
245 should "add the note to the journal" do
192 put '/issues/6.xml', @parameters, @headers
246 put '/issues/6.xml', @parameters, @headers
193
247
194 journal = Journal.last
248 journal = Journal.last
195 assert_equal "A new note", journal.notes
249 assert_equal "A new note", journal.notes
196 end
250 end
197
251
198 should "update the issue" do
252 should "update the issue" do
199 put '/issues/6.xml', @parameters, @headers
253 put '/issues/6.xml', @parameters, @headers
200
254
201 issue = Issue.find(6)
255 issue = Issue.find(6)
202 assert_equal "API update", issue.subject
256 assert_equal "API update", issue.subject
203 end
257 end
204
258
205 end
259 end
206
260
207 context "PUT /issues/6.xml with failed update" do
261 context "PUT /issues/6.xml with failed update" do
208 setup do
262 setup do
209 @parameters = {:issue => {:subject => ''}}
263 @parameters = {:issue => {:subject => ''}}
210 @headers = { :authorization => credentials('jsmith') }
264 @headers = { :authorization => credentials('jsmith') }
211 end
265 end
212
266
213 should_allow_api_authentication(:put,
267 should_allow_api_authentication(:put,
214 '/issues/6.xml',
268 '/issues/6.xml',
215 {:issue => {:subject => ''}}, # Missing subject should fail
269 {:issue => {:subject => ''}}, # Missing subject should fail
216 {:success_code => :unprocessable_entity})
270 {:success_code => :unprocessable_entity})
217
271
218 should "not create a new issue" do
272 should "not create a new issue" do
219 assert_no_difference('Issue.count') do
273 assert_no_difference('Issue.count') do
220 put '/issues/6.xml', @parameters, @headers
274 put '/issues/6.xml', @parameters, @headers
221 end
275 end
222 end
276 end
223
277
224 should "not create a new journal" do
278 should "not create a new journal" do
225 assert_no_difference('Journal.count') do
279 assert_no_difference('Journal.count') do
226 put '/issues/6.xml', @parameters, @headers
280 put '/issues/6.xml', @parameters, @headers
227 end
281 end
228 end
282 end
229
283
230 should "have an errors tag" do
284 should "have an errors tag" do
231 put '/issues/6.xml', @parameters, @headers
285 put '/issues/6.xml', @parameters, @headers
232
286
233 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
287 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
234 end
288 end
235 end
289 end
236
290
237 context "PUT /issues/6.json" do
291 context "PUT /issues/6.json" do
238 setup do
292 setup do
239 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
293 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
240 @headers = { :authorization => credentials('jsmith') }
294 @headers = { :authorization => credentials('jsmith') }
241 end
295 end
242
296
243 should_allow_api_authentication(:put,
297 should_allow_api_authentication(:put,
244 '/issues/6.json',
298 '/issues/6.json',
245 {:issue => {:subject => 'API update', :notes => 'A new note'}},
299 {:issue => {:subject => 'API update', :notes => 'A new note'}},
246 {:success_code => :ok})
300 {:success_code => :ok})
247
301
248 should "not create a new issue" do
302 should "not create a new issue" do
249 assert_no_difference('Issue.count') do
303 assert_no_difference('Issue.count') do
250 put '/issues/6.json', @parameters, @headers
304 put '/issues/6.json', @parameters, @headers
251 end
305 end
252 end
306 end
253
307
254 should "create a new journal" do
308 should "create a new journal" do
255 assert_difference('Journal.count') do
309 assert_difference('Journal.count') do
256 put '/issues/6.json', @parameters, @headers
310 put '/issues/6.json', @parameters, @headers
257 end
311 end
258 end
312 end
259
313
260 should "add the note to the journal" do
314 should "add the note to the journal" do
261 put '/issues/6.json', @parameters, @headers
315 put '/issues/6.json', @parameters, @headers
262
316
263 journal = Journal.last
317 journal = Journal.last
264 assert_equal "A new note", journal.notes
318 assert_equal "A new note", journal.notes
265 end
319 end
266
320
267 should "update the issue" do
321 should "update the issue" do
268 put '/issues/6.json', @parameters, @headers
322 put '/issues/6.json', @parameters, @headers
269
323
270 issue = Issue.find(6)
324 issue = Issue.find(6)
271 assert_equal "API update", issue.subject
325 assert_equal "API update", issue.subject
272 end
326 end
273
327
274 end
328 end
275
329
276 context "PUT /issues/6.json with failed update" do
330 context "PUT /issues/6.json with failed update" do
277 setup do
331 setup do
278 @parameters = {:issue => {:subject => ''}}
332 @parameters = {:issue => {:subject => ''}}
279 @headers = { :authorization => credentials('jsmith') }
333 @headers = { :authorization => credentials('jsmith') }
280 end
334 end
281
335
282 should_allow_api_authentication(:put,
336 should_allow_api_authentication(:put,
283 '/issues/6.json',
337 '/issues/6.json',
284 {:issue => {:subject => ''}}, # Missing subject should fail
338 {:issue => {:subject => ''}}, # Missing subject should fail
285 {:success_code => :unprocessable_entity})
339 {:success_code => :unprocessable_entity})
286
340
287 should "not create a new issue" do
341 should "not create a new issue" do
288 assert_no_difference('Issue.count') do
342 assert_no_difference('Issue.count') do
289 put '/issues/6.json', @parameters, @headers
343 put '/issues/6.json', @parameters, @headers
290 end
344 end
291 end
345 end
292
346
293 should "not create a new journal" do
347 should "not create a new journal" do
294 assert_no_difference('Journal.count') do
348 assert_no_difference('Journal.count') do
295 put '/issues/6.json', @parameters, @headers
349 put '/issues/6.json', @parameters, @headers
296 end
350 end
297 end
351 end
298
352
299 should "have an errors attribute" do
353 should "have an errors attribute" do
300 put '/issues/6.json', @parameters, @headers
354 put '/issues/6.json', @parameters, @headers
301
355
302 json = ActiveSupport::JSON.decode(response.body)
356 json = ActiveSupport::JSON.decode(response.body)
303 assert json['errors'].include?(['subject', "can't be blank"])
357 assert json['errors'].include?(['subject', "can't be blank"])
304 end
358 end
305 end
359 end
306
360
307 context "DELETE /issues/1.xml" do
361 context "DELETE /issues/1.xml" do
308 should_allow_api_authentication(:delete,
362 should_allow_api_authentication(:delete,
309 '/issues/6.xml',
363 '/issues/6.xml',
310 {},
364 {},
311 {:success_code => :ok})
365 {:success_code => :ok})
312
366
313 should "delete the issue" do
367 should "delete the issue" do
314 assert_difference('Issue.count',-1) do
368 assert_difference('Issue.count',-1) do
315 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
369 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
316 end
370 end
317
371
318 assert_nil Issue.find_by_id(6)
372 assert_nil Issue.find_by_id(6)
319 end
373 end
320 end
374 end
321
375
322 context "DELETE /issues/1.json" do
376 context "DELETE /issues/1.json" do
323 should_allow_api_authentication(:delete,
377 should_allow_api_authentication(:delete,
324 '/issues/6.json',
378 '/issues/6.json',
325 {},
379 {},
326 {:success_code => :ok})
380 {:success_code => :ok})
327
381
328 should "delete the issue" do
382 should "delete the issue" do
329 assert_difference('Issue.count',-1) do
383 assert_difference('Issue.count',-1) do
330 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
384 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
331 end
385 end
332
386
333 assert_nil Issue.find_by_id(6)
387 assert_nil Issue.find_by_id(6)
334 end
388 end
335 end
389 end
336
390
337 def credentials(user, password=nil)
391 def credentials(user, password=nil)
338 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
392 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
339 end
393 end
340 end
394 end
General Comments 0
You need to be logged in to leave comments. Login now