##// END OF EJS Templates
Adds REST API for issue relations (#7366)....
Jean-Philippe Lang -
r6056:21b37187445f
parent child
Show More
@@ -0,0 +1,7
1 api.relation do
2 api.id @relation.id
3 api.issue_id @relation.issue_from_id
4 api.issue_to_id @relation.issue_to_id
5 api.relation_type @relation.relation_type_for(@issue)
6 api.delay @relation.delay
7 end
@@ -0,0 +1,83
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../test_helper', __FILE__)
19
20 class ApiTest::IssueRelationsTest < ActionController::IntegrationTest
21 fixtures :all
22
23 def setup
24 Setting.rest_api_enabled = '1'
25 end
26
27 context "/issues/:issue_id/relations" do
28 context "POST" do
29 should "create a relation" do
30 assert_difference('IssueRelation.count') do
31 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, :authorization => credentials('jsmith')
32 end
33
34 relation = IssueRelation.first(:order => 'id DESC')
35 assert_equal 2, relation.issue_from_id
36 assert_equal 7, relation.issue_to_id
37 assert_equal 'relates', relation.relation_type
38
39 assert_response :created
40 assert_equal 'application/xml', @response.content_type
41 assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s}
42 end
43
44 context "with failure" do
45 should "return the errors" do
46 assert_no_difference('IssueRelation.count') do
47 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, :authorization => credentials('jsmith')
48 end
49
50 assert_response :unprocessable_entity
51 assert_tag :errors, :child => {:tag => 'error', :content => 'relation_type is not included in the list'}
52 end
53 end
54 end
55 end
56
57 context "/issues/:issue_id/relations/:id" do
58 context "GET" do
59 should "return the relation" do
60 get '/issues/3/relations/2.xml', {}, :authorization => credentials('jsmith')
61
62 assert_response :success
63 assert_equal 'application/xml', @response.content_type
64 assert_tag 'relation', :child => {:tag => 'id', :content => '2'}
65 end
66 end
67
68 context "DELETE" do
69 should "delete the relation" do
70 assert_difference('IssueRelation.count', -1) do
71 delete '/issues/3/relations/2.xml', {}, :authorization => credentials('jsmith')
72 end
73
74 assert_response :ok
75 assert_nil IssueRelation.find_by_id(2)
76 end
77 end
78 end
79
80 def credentials(user, password=nil)
81 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
82 end
83 end
@@ -1,64 +1,87
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_issue, :find_project_from_association, :authorize
20 accept_key_auth :show, :create, :destroy
20 21
21 def new
22 def show
23 @relation = @issue.find_relation(params[:id])
24
25 respond_to do |format|
26 format.html { render :nothing => true }
27 format.api
28 end
29 rescue ActiveRecord::RecordNotFound
30 render_404
31 end
32
33 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
34 def create
22 35 @relation = IssueRelation.new(params[:relation])
23 36 @relation.issue_from = @issue
24 37 if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/)
25 38 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
26 39 end
27 @relation.save if request.post?
40 saved = @relation.save
41
28 42 respond_to do |format|
29 43 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
30 44 format.js do
31 45 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
32 46 render :update do |page|
33 47 page.replace_html "relations", :partial => 'issues/relations'
34 48 if @relation.errors.empty?
35 49 page << "$('relation_delay').value = ''"
36 50 page << "$('relation_issue_to_id').value = ''"
37 51 end
38 52 end
39 53 end
54 format.api {
55 if saved
56 render :action => 'show', :status => :created, :location => issue_relation_url(@issue, @relation)
57 else
58 render_validation_errors(@relation)
59 end
60 }
40 61 end
41 62 end
42 63
64 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
43 65 def destroy
44 relation = IssueRelation.find(params[:id])
45 if request.post? && @issue.relations.include?(relation)
66 relation = @issue.find_relation(params[:id])
46 67 relation.destroy
47 @issue.reload
48 end
68
49 69 respond_to do |format|
50 70 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
51 71 format.js {
52 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
72 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
53 73 render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'}
54 74 }
75 format.api { head :ok }
55 76 end
77 rescue ActiveRecord::RecordNotFound
78 render_404
56 79 end
57 80
58 81 private
59 82 def find_issue
60 83 @issue = @object = Issue.find(params[:issue_id])
61 84 rescue ActiveRecord::RecordNotFound
62 85 render_404
63 86 end
64 87 end
@@ -1,930 +1,935
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61
62 62 named_scope :visible, lambda {|*args| { :include => :project,
63 63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 64
65 65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66 66
67 67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71 71
72 72 named_scope :without_version, lambda {
73 73 {
74 74 :conditions => { :fixed_version_id => nil}
75 75 }
76 76 }
77 77
78 78 named_scope :with_query, lambda {|query|
79 79 {
80 80 :conditions => Query.merge_conditions(query.statement)
81 81 }
82 82 }
83 83
84 84 before_create :default_assign
85 85 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 87 after_destroy :update_parent_attributes
88 88
89 89 # Returns a SQL conditions string used to find all issues visible by the specified user
90 90 def self.visible_condition(user, options={})
91 91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 92 case role.issues_visibility
93 93 when 'all'
94 94 nil
95 95 when 'default'
96 96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
97 97 when 'own'
98 98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
99 99 else
100 100 '1=0'
101 101 end
102 102 end
103 103 end
104 104
105 105 # Returns true if usr or current user is allowed to view the issue
106 106 def visible?(usr=nil)
107 107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108 108 case role.issues_visibility
109 109 when 'all'
110 110 true
111 111 when 'default'
112 112 !self.is_private? || self.author == user || self.assigned_to == user
113 113 when 'own'
114 114 self.author == user || self.assigned_to == user
115 115 else
116 116 false
117 117 end
118 118 end
119 119 end
120 120
121 121 def after_initialize
122 122 if new_record?
123 123 # set default values for new records only
124 124 self.status ||= IssueStatus.default
125 125 self.priority ||= IssuePriority.default
126 126 end
127 127 end
128 128
129 129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
130 130 def available_custom_fields
131 131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
132 132 end
133 133
134 134 def copy_from(arg)
135 135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
136 136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
137 137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 138 self.status = issue.status
139 139 self
140 140 end
141 141
142 142 # Moves/copies an issue to a new project and tracker
143 143 # Returns the moved/copied issue on success, false on failure
144 144 def move_to_project(*args)
145 145 ret = Issue.transaction do
146 146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
147 147 end || false
148 148 end
149 149
150 150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
151 151 options ||= {}
152 152 issue = options[:copy] ? self.class.new.copy_from(self) : self
153 153
154 154 if new_project && issue.project_id != new_project.id
155 155 # delete issue relations
156 156 unless Setting.cross_project_issue_relations?
157 157 issue.relations_from.clear
158 158 issue.relations_to.clear
159 159 end
160 160 # issue is moved to another project
161 161 # reassign to the category with same name if any
162 162 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
163 163 issue.category = new_category
164 164 # Keep the fixed_version if it's still valid in the new_project
165 165 unless new_project.shared_versions.include?(issue.fixed_version)
166 166 issue.fixed_version = nil
167 167 end
168 168 issue.project = new_project
169 169 if issue.parent && issue.parent.project_id != issue.project_id
170 170 issue.parent_issue_id = nil
171 171 end
172 172 end
173 173 if new_tracker
174 174 issue.tracker = new_tracker
175 175 issue.reset_custom_values!
176 176 end
177 177 if options[:copy]
178 178 issue.author = User.current
179 179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
180 180 issue.status = if options[:attributes] && options[:attributes][:status_id]
181 181 IssueStatus.find_by_id(options[:attributes][:status_id])
182 182 else
183 183 self.status
184 184 end
185 185 end
186 186 # Allow bulk setting of attributes on the issue
187 187 if options[:attributes]
188 188 issue.attributes = options[:attributes]
189 189 end
190 190 if issue.save
191 191 if options[:copy]
192 192 if current_journal && current_journal.notes.present?
193 193 issue.init_journal(current_journal.user, current_journal.notes)
194 194 issue.current_journal.notify = false
195 195 issue.save
196 196 end
197 197 else
198 198 # Manually update project_id on related time entries
199 199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
200 200
201 201 issue.children.each do |child|
202 202 unless child.move_to_project_without_transaction(new_project)
203 203 # Move failed and transaction was rollback'd
204 204 return false
205 205 end
206 206 end
207 207 end
208 208 else
209 209 return false
210 210 end
211 211 issue
212 212 end
213 213
214 214 def status_id=(sid)
215 215 self.status = nil
216 216 write_attribute(:status_id, sid)
217 217 end
218 218
219 219 def priority_id=(pid)
220 220 self.priority = nil
221 221 write_attribute(:priority_id, pid)
222 222 end
223 223
224 224 def tracker_id=(tid)
225 225 self.tracker = nil
226 226 result = write_attribute(:tracker_id, tid)
227 227 @custom_field_values = nil
228 228 result
229 229 end
230 230
231 231 def description=(arg)
232 232 if arg.is_a?(String)
233 233 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
234 234 end
235 235 write_attribute(:description, arg)
236 236 end
237 237
238 238 # Overrides attributes= so that tracker_id gets assigned first
239 239 def attributes_with_tracker_first=(new_attributes, *args)
240 240 return if new_attributes.nil?
241 241 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
242 242 if new_tracker_id
243 243 self.tracker_id = new_tracker_id
244 244 end
245 245 send :attributes_without_tracker_first=, new_attributes, *args
246 246 end
247 247 # Do not redefine alias chain on reload (see #4838)
248 248 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
249 249
250 250 def estimated_hours=(h)
251 251 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
252 252 end
253 253
254 254 safe_attributes 'tracker_id',
255 255 'status_id',
256 256 'parent_issue_id',
257 257 'category_id',
258 258 'assigned_to_id',
259 259 'priority_id',
260 260 'fixed_version_id',
261 261 'subject',
262 262 'description',
263 263 'start_date',
264 264 'due_date',
265 265 'done_ratio',
266 266 'estimated_hours',
267 267 'custom_field_values',
268 268 'custom_fields',
269 269 'lock_version',
270 270 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
271 271
272 272 safe_attributes 'status_id',
273 273 'assigned_to_id',
274 274 'fixed_version_id',
275 275 'done_ratio',
276 276 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
277 277
278 278 safe_attributes 'is_private',
279 279 :if => lambda {|issue, user|
280 280 user.allowed_to?(:set_issues_private, issue.project) ||
281 281 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
282 282 }
283 283
284 284 # Safely sets attributes
285 285 # Should be called from controllers instead of #attributes=
286 286 # attr_accessible is too rough because we still want things like
287 287 # Issue.new(:project => foo) to work
288 288 # TODO: move workflow/permission checks from controllers to here
289 289 def safe_attributes=(attrs, user=User.current)
290 290 return unless attrs.is_a?(Hash)
291 291
292 292 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
293 293 attrs = delete_unsafe_attributes(attrs, user)
294 294 return if attrs.empty?
295 295
296 296 # Tracker must be set before since new_statuses_allowed_to depends on it.
297 297 if t = attrs.delete('tracker_id')
298 298 self.tracker_id = t
299 299 end
300 300
301 301 if attrs['status_id']
302 302 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
303 303 attrs.delete('status_id')
304 304 end
305 305 end
306 306
307 307 unless leaf?
308 308 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
309 309 end
310 310
311 311 if attrs.has_key?('parent_issue_id')
312 312 if !user.allowed_to?(:manage_subtasks, project)
313 313 attrs.delete('parent_issue_id')
314 314 elsif !attrs['parent_issue_id'].blank?
315 315 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
316 316 end
317 317 end
318 318
319 319 self.attributes = attrs
320 320 end
321 321
322 322 def done_ratio
323 323 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
324 324 status.default_done_ratio
325 325 else
326 326 read_attribute(:done_ratio)
327 327 end
328 328 end
329 329
330 330 def self.use_status_for_done_ratio?
331 331 Setting.issue_done_ratio == 'issue_status'
332 332 end
333 333
334 334 def self.use_field_for_done_ratio?
335 335 Setting.issue_done_ratio == 'issue_field'
336 336 end
337 337
338 338 def validate
339 339 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
340 340 errors.add :due_date, :not_a_date
341 341 end
342 342
343 343 if self.due_date and self.start_date and self.due_date < self.start_date
344 344 errors.add :due_date, :greater_than_start_date
345 345 end
346 346
347 347 if start_date && soonest_start && start_date < soonest_start
348 348 errors.add :start_date, :invalid
349 349 end
350 350
351 351 if fixed_version
352 352 if !assignable_versions.include?(fixed_version)
353 353 errors.add :fixed_version_id, :inclusion
354 354 elsif reopened? && fixed_version.closed?
355 355 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
356 356 end
357 357 end
358 358
359 359 # Checks that the issue can not be added/moved to a disabled tracker
360 360 if project && (tracker_id_changed? || project_id_changed?)
361 361 unless project.trackers.include?(tracker)
362 362 errors.add :tracker_id, :inclusion
363 363 end
364 364 end
365 365
366 366 # Checks parent issue assignment
367 367 if @parent_issue
368 368 if @parent_issue.project_id != project_id
369 369 errors.add :parent_issue_id, :not_same_project
370 370 elsif !new_record?
371 371 # moving an existing issue
372 372 if @parent_issue.root_id != root_id
373 373 # we can always move to another tree
374 374 elsif move_possible?(@parent_issue)
375 375 # move accepted inside tree
376 376 else
377 377 errors.add :parent_issue_id, :not_a_valid_parent
378 378 end
379 379 end
380 380 end
381 381 end
382 382
383 383 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
384 384 # even if the user turns off the setting later
385 385 def update_done_ratio_from_issue_status
386 386 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
387 387 self.done_ratio = status.default_done_ratio
388 388 end
389 389 end
390 390
391 391 def init_journal(user, notes = "")
392 392 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
393 393 @issue_before_change = self.clone
394 394 @issue_before_change.status = self.status
395 395 @custom_values_before_change = {}
396 396 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
397 397 # Make sure updated_on is updated when adding a note.
398 398 updated_on_will_change!
399 399 @current_journal
400 400 end
401 401
402 402 # Return true if the issue is closed, otherwise false
403 403 def closed?
404 404 self.status.is_closed?
405 405 end
406 406
407 407 # Return true if the issue is being reopened
408 408 def reopened?
409 409 if !new_record? && status_id_changed?
410 410 status_was = IssueStatus.find_by_id(status_id_was)
411 411 status_new = IssueStatus.find_by_id(status_id)
412 412 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
413 413 return true
414 414 end
415 415 end
416 416 false
417 417 end
418 418
419 419 # Return true if the issue is being closed
420 420 def closing?
421 421 if !new_record? && status_id_changed?
422 422 status_was = IssueStatus.find_by_id(status_id_was)
423 423 status_new = IssueStatus.find_by_id(status_id)
424 424 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
425 425 return true
426 426 end
427 427 end
428 428 false
429 429 end
430 430
431 431 # Returns true if the issue is overdue
432 432 def overdue?
433 433 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
434 434 end
435 435
436 436 # Is the amount of work done less than it should for the due date
437 437 def behind_schedule?
438 438 return false if start_date.nil? || due_date.nil?
439 439 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
440 440 return done_date <= Date.today
441 441 end
442 442
443 443 # Does this issue have children?
444 444 def children?
445 445 !leaf?
446 446 end
447 447
448 448 # Users the issue can be assigned to
449 449 def assignable_users
450 450 users = project.assignable_users
451 451 users << author if author
452 452 users.uniq.sort
453 453 end
454 454
455 455 # Versions that the issue can be assigned to
456 456 def assignable_versions
457 457 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
458 458 end
459 459
460 460 # Returns true if this issue is blocked by another issue that is still open
461 461 def blocked?
462 462 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
463 463 end
464 464
465 465 # Returns an array of status that user is able to apply
466 466 def new_statuses_allowed_to(user, include_default=false)
467 467 statuses = status.find_new_statuses_allowed_to(
468 468 user.roles_for_project(project),
469 469 tracker,
470 470 author == user,
471 471 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
472 472 )
473 473 statuses << status unless statuses.empty?
474 474 statuses << IssueStatus.default if include_default
475 475 statuses = statuses.uniq.sort
476 476 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
477 477 end
478 478
479 479 # Returns the mail adresses of users that should be notified
480 480 def recipients
481 481 notified = project.notified_users
482 482 # Author and assignee are always notified unless they have been
483 483 # locked or don't want to be notified
484 484 notified << author if author && author.active? && author.notify_about?(self)
485 485 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
486 486 notified.uniq!
487 487 # Remove users that can not view the issue
488 488 notified.reject! {|user| !visible?(user)}
489 489 notified.collect(&:mail)
490 490 end
491 491
492 492 # Returns the total number of hours spent on this issue and its descendants
493 493 #
494 494 # Example:
495 495 # spent_hours => 0.0
496 496 # spent_hours => 50.2
497 497 def spent_hours
498 498 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
499 499 end
500 500
501 501 def relations
502 502 (relations_from + relations_to).sort
503 503 end
504 504
505 # Finds an issue relation given its id.
506 def find_relation(relation_id)
507 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
508 end
509
505 510 def all_dependent_issues(except=[])
506 511 except << self
507 512 dependencies = []
508 513 relations_from.each do |relation|
509 514 if relation.issue_to && !except.include?(relation.issue_to)
510 515 dependencies << relation.issue_to
511 516 dependencies += relation.issue_to.all_dependent_issues(except)
512 517 end
513 518 end
514 519 dependencies
515 520 end
516 521
517 522 # Returns an array of issues that duplicate this one
518 523 def duplicates
519 524 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
520 525 end
521 526
522 527 # Returns the due date or the target due date if any
523 528 # Used on gantt chart
524 529 def due_before
525 530 due_date || (fixed_version ? fixed_version.effective_date : nil)
526 531 end
527 532
528 533 # Returns the time scheduled for this issue.
529 534 #
530 535 # Example:
531 536 # Start Date: 2/26/09, End Date: 3/04/09
532 537 # duration => 6
533 538 def duration
534 539 (start_date && due_date) ? due_date - start_date : 0
535 540 end
536 541
537 542 def soonest_start
538 543 @soonest_start ||= (
539 544 relations_to.collect{|relation| relation.successor_soonest_start} +
540 545 ancestors.collect(&:soonest_start)
541 546 ).compact.max
542 547 end
543 548
544 549 def reschedule_after(date)
545 550 return if date.nil?
546 551 if leaf?
547 552 if start_date.nil? || start_date < date
548 553 self.start_date, self.due_date = date, date + duration
549 554 save
550 555 end
551 556 else
552 557 leaves.each do |leaf|
553 558 leaf.reschedule_after(date)
554 559 end
555 560 end
556 561 end
557 562
558 563 def <=>(issue)
559 564 if issue.nil?
560 565 -1
561 566 elsif root_id != issue.root_id
562 567 (root_id || 0) <=> (issue.root_id || 0)
563 568 else
564 569 (lft || 0) <=> (issue.lft || 0)
565 570 end
566 571 end
567 572
568 573 def to_s
569 574 "#{tracker} ##{id}: #{subject}"
570 575 end
571 576
572 577 # Returns a string of css classes that apply to the issue
573 578 def css_classes
574 579 s = "issue status-#{status.position} priority-#{priority.position}"
575 580 s << ' closed' if closed?
576 581 s << ' overdue' if overdue?
577 582 s << ' child' if child?
578 583 s << ' parent' unless leaf?
579 584 s << ' private' if is_private?
580 585 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
581 586 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
582 587 s
583 588 end
584 589
585 590 # Saves an issue, time_entry, attachments, and a journal from the parameters
586 591 # Returns false if save fails
587 592 def save_issue_with_child_records(params, existing_time_entry=nil)
588 593 Issue.transaction do
589 594 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
590 595 @time_entry = existing_time_entry || TimeEntry.new
591 596 @time_entry.project = project
592 597 @time_entry.issue = self
593 598 @time_entry.user = User.current
594 599 @time_entry.spent_on = Date.today
595 600 @time_entry.attributes = params[:time_entry]
596 601 self.time_entries << @time_entry
597 602 end
598 603
599 604 if valid?
600 605 attachments = Attachment.attach_files(self, params[:attachments])
601 606
602 607 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
603 608 # TODO: Rename hook
604 609 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
605 610 begin
606 611 if save
607 612 # TODO: Rename hook
608 613 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
609 614 else
610 615 raise ActiveRecord::Rollback
611 616 end
612 617 rescue ActiveRecord::StaleObjectError
613 618 attachments[:files].each(&:destroy)
614 619 errors.add_to_base l(:notice_locking_conflict)
615 620 raise ActiveRecord::Rollback
616 621 end
617 622 end
618 623 end
619 624 end
620 625
621 626 # Unassigns issues from +version+ if it's no longer shared with issue's project
622 627 def self.update_versions_from_sharing_change(version)
623 628 # Update issues assigned to the version
624 629 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
625 630 end
626 631
627 632 # Unassigns issues from versions that are no longer shared
628 633 # after +project+ was moved
629 634 def self.update_versions_from_hierarchy_change(project)
630 635 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
631 636 # Update issues of the moved projects and issues assigned to a version of a moved project
632 637 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
633 638 end
634 639
635 640 def parent_issue_id=(arg)
636 641 parent_issue_id = arg.blank? ? nil : arg.to_i
637 642 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
638 643 @parent_issue.id
639 644 else
640 645 @parent_issue = nil
641 646 nil
642 647 end
643 648 end
644 649
645 650 def parent_issue_id
646 651 if instance_variable_defined? :@parent_issue
647 652 @parent_issue.nil? ? nil : @parent_issue.id
648 653 else
649 654 parent_id
650 655 end
651 656 end
652 657
653 658 # Extracted from the ReportsController.
654 659 def self.by_tracker(project)
655 660 count_and_group_by(:project => project,
656 661 :field => 'tracker_id',
657 662 :joins => Tracker.table_name)
658 663 end
659 664
660 665 def self.by_version(project)
661 666 count_and_group_by(:project => project,
662 667 :field => 'fixed_version_id',
663 668 :joins => Version.table_name)
664 669 end
665 670
666 671 def self.by_priority(project)
667 672 count_and_group_by(:project => project,
668 673 :field => 'priority_id',
669 674 :joins => IssuePriority.table_name)
670 675 end
671 676
672 677 def self.by_category(project)
673 678 count_and_group_by(:project => project,
674 679 :field => 'category_id',
675 680 :joins => IssueCategory.table_name)
676 681 end
677 682
678 683 def self.by_assigned_to(project)
679 684 count_and_group_by(:project => project,
680 685 :field => 'assigned_to_id',
681 686 :joins => User.table_name)
682 687 end
683 688
684 689 def self.by_author(project)
685 690 count_and_group_by(:project => project,
686 691 :field => 'author_id',
687 692 :joins => User.table_name)
688 693 end
689 694
690 695 def self.by_subproject(project)
691 696 ActiveRecord::Base.connection.select_all("select s.id as status_id,
692 697 s.is_closed as closed,
693 698 #{Issue.table_name}.project_id as project_id,
694 699 count(#{Issue.table_name}.id) as total
695 700 from
696 701 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
697 702 where
698 703 #{Issue.table_name}.status_id=s.id
699 704 and #{Issue.table_name}.project_id = #{Project.table_name}.id
700 705 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
701 706 and #{Issue.table_name}.project_id <> #{project.id}
702 707 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
703 708 end
704 709 # End ReportsController extraction
705 710
706 711 # Returns an array of projects that current user can move issues to
707 712 def self.allowed_target_projects_on_move
708 713 projects = []
709 714 if User.current.admin?
710 715 # admin is allowed to move issues to any active (visible) project
711 716 projects = Project.visible.all
712 717 elsif User.current.logged?
713 718 if Role.non_member.allowed_to?(:move_issues)
714 719 projects = Project.visible.all
715 720 else
716 721 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
717 722 end
718 723 end
719 724 projects
720 725 end
721 726
722 727 private
723 728
724 729 def update_nested_set_attributes
725 730 if root_id.nil?
726 731 # issue was just created
727 732 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
728 733 set_default_left_and_right
729 734 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
730 735 if @parent_issue
731 736 move_to_child_of(@parent_issue)
732 737 end
733 738 reload
734 739 elsif parent_issue_id != parent_id
735 740 former_parent_id = parent_id
736 741 # moving an existing issue
737 742 if @parent_issue && @parent_issue.root_id == root_id
738 743 # inside the same tree
739 744 move_to_child_of(@parent_issue)
740 745 else
741 746 # to another tree
742 747 unless root?
743 748 move_to_right_of(root)
744 749 reload
745 750 end
746 751 old_root_id = root_id
747 752 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
748 753 target_maxright = nested_set_scope.maximum(right_column_name) || 0
749 754 offset = target_maxright + 1 - lft
750 755 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
751 756 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
752 757 self[left_column_name] = lft + offset
753 758 self[right_column_name] = rgt + offset
754 759 if @parent_issue
755 760 move_to_child_of(@parent_issue)
756 761 end
757 762 end
758 763 reload
759 764 # delete invalid relations of all descendants
760 765 self_and_descendants.each do |issue|
761 766 issue.relations.each do |relation|
762 767 relation.destroy unless relation.valid?
763 768 end
764 769 end
765 770 # update former parent
766 771 recalculate_attributes_for(former_parent_id) if former_parent_id
767 772 end
768 773 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
769 774 end
770 775
771 776 def update_parent_attributes
772 777 recalculate_attributes_for(parent_id) if parent_id
773 778 end
774 779
775 780 def recalculate_attributes_for(issue_id)
776 781 if issue_id && p = Issue.find_by_id(issue_id)
777 782 # priority = highest priority of children
778 783 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
779 784 p.priority = IssuePriority.find_by_position(priority_position)
780 785 end
781 786
782 787 # start/due dates = lowest/highest dates of children
783 788 p.start_date = p.children.minimum(:start_date)
784 789 p.due_date = p.children.maximum(:due_date)
785 790 if p.start_date && p.due_date && p.due_date < p.start_date
786 791 p.start_date, p.due_date = p.due_date, p.start_date
787 792 end
788 793
789 794 # done ratio = weighted average ratio of leaves
790 795 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
791 796 leaves_count = p.leaves.count
792 797 if leaves_count > 0
793 798 average = p.leaves.average(:estimated_hours).to_f
794 799 if average == 0
795 800 average = 1
796 801 end
797 802 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
798 803 progress = done / (average * leaves_count)
799 804 p.done_ratio = progress.round
800 805 end
801 806 end
802 807
803 808 # estimate = sum of leaves estimates
804 809 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
805 810 p.estimated_hours = nil if p.estimated_hours == 0.0
806 811
807 812 # ancestors will be recursively updated
808 813 p.save(false)
809 814 end
810 815 end
811 816
812 817 # Update issues so their versions are not pointing to a
813 818 # fixed_version that is not shared with the issue's project
814 819 def self.update_versions(conditions=nil)
815 820 # Only need to update issues with a fixed_version from
816 821 # a different project and that is not systemwide shared
817 822 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
818 823 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
819 824 " AND #{Version.table_name}.sharing <> 'system'",
820 825 conditions),
821 826 :include => [:project, :fixed_version]
822 827 ).each do |issue|
823 828 next if issue.project.nil? || issue.fixed_version.nil?
824 829 unless issue.project.shared_versions.include?(issue.fixed_version)
825 830 issue.init_journal(User.current)
826 831 issue.fixed_version = nil
827 832 issue.save
828 833 end
829 834 end
830 835 end
831 836
832 837 # Callback on attachment deletion
833 838 def attachment_removed(obj)
834 839 journal = init_journal(User.current)
835 840 journal.details << JournalDetail.new(:property => 'attachment',
836 841 :prop_key => obj.id,
837 842 :old_value => obj.filename)
838 843 journal.save
839 844 end
840 845
841 846 # Default assignment based on category
842 847 def default_assign
843 848 if assigned_to.nil? && category && category.assigned_to
844 849 self.assigned_to = category.assigned_to
845 850 end
846 851 end
847 852
848 853 # Updates start/due dates of following issues
849 854 def reschedule_following_issues
850 855 if start_date_changed? || due_date_changed?
851 856 relations_from.each do |relation|
852 857 relation.set_issue_to_dates
853 858 end
854 859 end
855 860 end
856 861
857 862 # Closes duplicates if the issue is being closed
858 863 def close_duplicates
859 864 if closing?
860 865 duplicates.each do |duplicate|
861 866 # Reload is need in case the duplicate was updated by a previous duplicate
862 867 duplicate.reload
863 868 # Don't re-close it if it's already closed
864 869 next if duplicate.closed?
865 870 # Same user and notes
866 871 if @current_journal
867 872 duplicate.init_journal(@current_journal.user, @current_journal.notes)
868 873 end
869 874 duplicate.update_attribute :status, self.status
870 875 end
871 876 end
872 877 end
873 878
874 879 # Saves the changes in a Journal
875 880 # Called after_save
876 881 def create_journal
877 882 if @current_journal
878 883 # attributes changes
879 884 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
880 885 before = @issue_before_change.send(c)
881 886 after = send(c)
882 887 next if before == after || (before.blank? && after.blank?)
883 888 @current_journal.details << JournalDetail.new(:property => 'attr',
884 889 :prop_key => c,
885 890 :old_value => @issue_before_change.send(c),
886 891 :value => send(c))
887 892 }
888 893 # custom fields changes
889 894 custom_values.each {|c|
890 895 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
891 896 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
892 897 @current_journal.details << JournalDetail.new(:property => 'cf',
893 898 :prop_key => c.custom_field_id,
894 899 :old_value => @custom_values_before_change[c.custom_field_id],
895 900 :value => c.value)
896 901 }
897 902 @current_journal.save
898 903 # reset current journal
899 904 init_journal @current_journal.user, @current_journal.notes
900 905 end
901 906 end
902 907
903 908 # Query generator for selecting groups of issue counts for a project
904 909 # based on specific criteria
905 910 #
906 911 # Options
907 912 # * project - Project to search in.
908 913 # * field - String. Issue field to key off of in the grouping.
909 914 # * joins - String. The table name to join against.
910 915 def self.count_and_group_by(options)
911 916 project = options.delete(:project)
912 917 select_field = options.delete(:field)
913 918 joins = options.delete(:joins)
914 919
915 920 where = "#{Issue.table_name}.#{select_field}=j.id"
916 921
917 922 ActiveRecord::Base.connection.select_all("select s.id as status_id,
918 923 s.is_closed as closed,
919 924 j.id as #{select_field},
920 925 count(#{Issue.table_name}.id) as total
921 926 from
922 927 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
923 928 where
924 929 #{Issue.table_name}.status_id=s.id
925 930 and #{where}
926 931 and #{Issue.table_name}.project_id=#{Project.table_name}.id
927 932 and #{visible_condition(User.current, :project => project)}
928 933 group by s.id, s.is_closed, j.id")
929 934 end
930 935 end
@@ -1,37 +1,37
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
3 3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <p><strong><%=l(:label_related_issues)%></strong></p>
8 8
9 9 <% if @relations.present? %>
10 10 <form>
11 11 <table class="list issues">
12 12 <% @relations.each do |relation| %>
13 13 <tr class="issue hascontextmenu">
14 14 <td class="checkbox"><%= check_box_tag("ids[]", relation.other_issue(@issue).id, false, :id => nil) %></td>
15 15 <td class="subject"><%= l(relation.label_for(@issue)) %> <%= "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)})" if relation.delay && relation.delay != 0 %>
16 16 <%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %>
17 17 <%= link_to_issue(relation.other_issue(@issue), :truncate => 60) %>
18 18 </td>
19 19 <td class="status"><%= relation.other_issue(@issue).status.name %></td>
20 20 <td class="start_date"><%= format_date(relation.other_issue(@issue).start_date) %></td>
21 21 <td class="due_date"><%= format_date(relation.other_issue(@issue).due_date) %></td>
22 22 <td class="buttons"><%= link_to_remote(image_tag('link_break.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation},
23 :method => :post
23 :method => :delete
24 24 }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %></td>
25 25 </tr>
26 26 <% end %>
27 27 </table>
28 28 </form>
29 29 <% end %>
30 30
31 31 <% remote_form_for(:relation, @relation,
32 :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue},
32 :url => {:controller => 'issue_relations', :action => 'create', :issue_id => @issue},
33 33 :method => :post,
34 34 :complete => "Form.Element.focus('relation_issue_to_id');",
35 35 :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %>
36 36 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
37 37 <% end %>
@@ -1,61 +1,61
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 api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project)
20 20
21 21 render_api_custom_values @issue.custom_field_values, api
22 22
23 23 api.created_on @issue.created_on
24 24 api.updated_on @issue.updated_on
25 25
26 26 render_api_issue_children(@issue, api) if include_in_api_response?('children')
27 27
28 28 api.array :relations do
29 29 @relations.each do |relation|
30 api.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
30 api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
31 31 end
32 32 end if include_in_api_response?('relations') && @relations.present?
33 33
34 34 api.array :changesets do
35 35 @issue.changesets.each do |changeset|
36 36 api.changeset :revision => changeset.revision do
37 37 api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
38 38 api.comments changeset.comments
39 39 api.committed_on changeset.committed_on
40 40 end
41 41 end
42 42 end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project)
43 43
44 44 api.array :journals do
45 45 @issue.journals.each do |journal|
46 46 api.journal :id => journal.id do
47 47 api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
48 48 api.notes journal.notes
49 49 api.created_on journal.created_on
50 50 api.array :details do
51 51 journal.details.each do |detail|
52 52 api.detail :property => detail.property, :name => detail.prop_key do
53 53 api.old_value detail.old_value
54 54 api.new_value detail.value
55 55 end
56 56 end
57 57 end
58 58 end
59 59 end
60 60 end if include_in_api_response?('journals')
61 61 end
@@ -1,260 +1,255
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome'
10 10
11 11 map.signin 'login', :controller => 'account', :action => 'login'
12 12 map.signout 'logout', :controller => 'account', :action => 'logout'
13 13
14 14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 16
17 17 map.with_options :controller => 'time_entry_reports', :action => 'report',:conditions => {:method => :get} do |time_report|
18 18 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report'
19 19 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report.:format'
20 20 time_report.connect 'projects/:project_id/time_entries/report'
21 21 time_report.connect 'projects/:project_id/time_entries/report.:format'
22 22 time_report.connect 'time_entries/report'
23 23 time_report.connect 'time_entries/report.:format'
24 24 end
25 25
26 26 map.bulk_edit_time_entry 'time_entries/bulk_edit',
27 27 :controller => 'timelog', :action => 'bulk_edit', :conditions => { :method => :get }
28 28 map.bulk_update_time_entry 'time_entries/bulk_edit',
29 29 :controller => 'timelog', :action => 'bulk_update', :conditions => { :method => :post }
30 30 map.time_entries_context_menu '/time_entries/context_menu',
31 31 :controller => 'context_menus', :action => 'time_entries'
32 32 # TODO: wasteful since this is also nested under issues, projects, and projects/issues
33 33 map.resources :time_entries, :controller => 'timelog'
34 34
35 35 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
36 36 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
37 37 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
38 38
39 39 map.with_options :controller => 'messages' do |messages_routes|
40 40 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
41 41 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
42 42 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
43 43 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
44 44 end
45 45 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
46 46 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
47 47 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
48 48 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
49 49 end
50 50 end
51 51
52 52 map.with_options :controller => 'boards' do |board_routes|
53 53 board_routes.with_options :conditions => {:method => :get} do |board_views|
54 54 board_views.connect 'projects/:project_id/boards', :action => 'index'
55 55 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
56 56 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
57 57 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
58 58 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
59 59 end
60 60 board_routes.with_options :conditions => {:method => :post} do |board_actions|
61 61 board_actions.connect 'projects/:project_id/boards', :action => 'new'
62 62 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
63 63 end
64 64 end
65 65
66 66 map.with_options :controller => 'documents' do |document_routes|
67 67 document_routes.with_options :conditions => {:method => :get} do |document_views|
68 68 document_views.connect 'projects/:project_id/documents', :action => 'index'
69 69 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
70 70 document_views.connect 'documents/:id', :action => 'show'
71 71 document_views.connect 'documents/:id/edit', :action => 'edit'
72 72 end
73 73 document_routes.with_options :conditions => {:method => :post} do |document_actions|
74 74 document_actions.connect 'projects/:project_id/documents', :action => 'new'
75 75 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
76 76 end
77 77 end
78 78
79 79 map.resources :issue_moves, :only => [:new, :create], :path_prefix => '/issues', :as => 'move'
80 80
81 81 # Misc issue routes. TODO: move into resources
82 82 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes', :action => 'issues'
83 83 map.preview_issue '/issues/preview/:id', :controller => 'previews', :action => 'issue' # TODO: would look nicer as /issues/:id/preview
84 84 map.issues_context_menu '/issues/context_menu', :controller => 'context_menus', :action => 'issues'
85 85 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
86 86 map.bulk_edit_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_edit', :conditions => { :method => :get }
87 87 map.bulk_update_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_update', :conditions => { :method => :post }
88 88 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/, :conditions => { :method => :post }
89 89 map.connect '/issues/:id/destroy', :controller => 'issues', :action => 'destroy', :conditions => { :method => :post } # legacy
90 90
91 91 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
92 92 gantts_routes.connect '/projects/:project_id/issues/gantt'
93 93 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
94 94 gantts_routes.connect '/issues/gantt.:format'
95 95 end
96 96
97 97 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
98 98 calendars_routes.connect '/projects/:project_id/issues/calendar'
99 99 calendars_routes.connect '/issues/calendar'
100 100 end
101 101
102 102 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
103 103 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
104 104 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
105 105 end
106 106
107 107 # Following two routes conflict with the resources because #index allows POST
108 108 map.connect '/issues', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
109 109 map.connect '/issues/create', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
110 110
111 111 map.resources :issues, :member => { :edit => :post }, :collection => {} do |issues|
112 112 issues.resources :time_entries, :controller => 'timelog'
113 issues.resources :relations, :controller => 'issue_relations', :only => [:show, :create, :destroy]
113 114 end
114 115
115 116 map.resources :issues, :path_prefix => '/projects/:project_id', :collection => { :create => :post } do |issues|
116 117 issues.resources :time_entries, :controller => 'timelog'
117 118 end
118 119
119 map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
120 relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
121 relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
122 end
123
124 120 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
125 121
126 122 map.with_options :controller => 'users' do |users|
127 123 users.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil, :conditions => {:method => :get}
128 124
129 125 users.with_options :conditions => {:method => :post} do |user_actions|
130 126 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
131 127 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
132 128 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
133 129 end
134 130 end
135 131
136 132 map.resources :users, :member => {
137 133 :edit_membership => :post,
138 134 :destroy_membership => :post
139 135 }
140 136
141 137 # For nice "roadmap" in the url for the index action
142 138 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
143 139
144 140 map.all_news 'news', :controller => 'news', :action => 'index'
145 141 map.formatted_all_news 'news.:format', :controller => 'news', :action => 'index'
146 142 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
147 143 map.connect 'news/:id/comments', :controller => 'comments', :action => 'create', :conditions => {:method => :post}
148 144 map.connect 'news/:id/comments/:comment_id', :controller => 'comments', :action => 'destroy', :conditions => {:method => :delete}
149 145
150 146 map.resources :projects, :member => {
151 147 :copy => [:get, :post],
152 148 :settings => :get,
153 149 :modules => :post,
154 150 :archive => :post,
155 151 :unarchive => :post
156 152 } do |project|
157 153 project.resource :project_enumerations, :as => 'enumerations', :only => [:update, :destroy]
158 154 project.resources :files, :only => [:index, :new, :create]
159 155 project.resources :versions, :collection => {:close_completed => :put}, :member => {:status_by => :post}
160 156 project.resources :news, :shallow => true
161 157 project.resources :time_entries, :controller => 'timelog', :path_prefix => 'projects/:project_id'
162 158
163 159 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
164 160 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
165 161 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
166 162 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
167 163 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
168 164 project.resources :wiki, :except => [:new, :create], :member => {
169 165 :rename => [:get, :post],
170 166 :history => :get,
171 167 :preview => :any,
172 168 :protect => :post,
173 169 :add_attachment => :post
174 170 }, :collection => {
175 171 :export => :get,
176 172 :date_index => :get
177 173 }
178 174
179 175 end
180 176
181 177 # Destroy uses a get request to prompt the user before the actual DELETE request
182 178 map.project_destroy_confirm 'projects/:id/destroy', :controller => 'projects', :action => 'destroy', :conditions => {:method => :get}
183 179
184 180 # TODO: port to be part of the resources route(s)
185 181 map.with_options :controller => 'projects' do |project_mapper|
186 182 project_mapper.with_options :conditions => {:method => :get} do |project_views|
187 183 project_views.connect 'projects/:id/settings/:tab', :controller => 'projects', :action => 'settings'
188 184 project_views.connect 'projects/:project_id/issues/:copy_from/copy', :controller => 'issues', :action => 'new'
189 185 end
190 186 end
191 187
192 188 map.with_options :controller => 'activities', :action => 'index', :conditions => {:method => :get} do |activity|
193 189 activity.connect 'projects/:id/activity'
194 190 activity.connect 'projects/:id/activity.:format'
195 191 activity.connect 'activity', :id => nil
196 192 activity.connect 'activity.:format', :id => nil
197 193 end
198 194
199 195
200 196 map.with_options :controller => 'issue_categories' do |categories|
201 197 categories.connect 'projects/:project_id/issue_categories/new', :action => 'new'
202 198 end
203 199
204 200 map.with_options :controller => 'repositories' do |repositories|
205 201 repositories.with_options :conditions => {:method => :get} do |repository_views|
206 202 repository_views.connect 'projects/:id/repository', :action => 'show'
207 203 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
208 204 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
209 205 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
210 206 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
211 207 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
212 208 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
213 209 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
214 210 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
215 211 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
216 212 repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
217 213 # TODO: why the following route is required?
218 214 repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
219 215 repository_views.connect 'projects/:id/repository/:action/*path'
220 216 end
221 217
222 218 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
223 219 end
224 220
225 221 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
226 222 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
227 223 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
228 224
229 225 map.resources :groups
230 226
231 227 #left old routes at the bottom for backwards compat
232 228 map.connect 'projects/:project_id/queries/:action', :controller => 'queries'
233 229 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
234 230 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
235 231 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
236 232 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
237 233 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
238 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
239 234 map.connect 'projects/:project_id/news/:action', :controller => 'news'
240 235 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
241 236 map.with_options :controller => 'repositories' do |omap|
242 237 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
243 238 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
244 239 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
245 240 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
246 241 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
247 242 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
248 243 end
249 244
250 245 map.with_options :controller => 'sys' do |sys|
251 246 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
252 247 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
253 248 end
254 249
255 250 # Install the default route as the lowest priority.
256 251 map.connect ':controller/:action/:id'
257 252 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
258 253 # Used for OpenID
259 254 map.root :controller => 'account', :action => 'login'
260 255 end
@@ -1,237 +1,237
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/activity'
4 4 require 'redmine/search'
5 5 require 'redmine/custom_field_format'
6 6 require 'redmine/mime_type'
7 7 require 'redmine/core_ext'
8 8 require 'redmine/themes'
9 9 require 'redmine/hook'
10 10 require 'redmine/plugin'
11 11 require 'redmine/notifiable'
12 12 require 'redmine/wiki_formatting'
13 13 require 'redmine/scm/base'
14 14
15 15 begin
16 16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
17 17 rescue LoadError
18 18 # RMagick is not available
19 19 end
20 20
21 21 if RUBY_VERSION < '1.9'
22 22 require 'faster_csv'
23 23 else
24 24 require 'csv'
25 25 FCSV = CSV
26 26 end
27 27
28 28 Redmine::Scm::Base.add "Subversion"
29 29 Redmine::Scm::Base.add "Darcs"
30 30 Redmine::Scm::Base.add "Mercurial"
31 31 Redmine::Scm::Base.add "Cvs"
32 32 Redmine::Scm::Base.add "Bazaar"
33 33 Redmine::Scm::Base.add "Git"
34 34 Redmine::Scm::Base.add "Filesystem"
35 35
36 36 Redmine::CustomFieldFormat.map do |fields|
37 37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
38 38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
39 39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
40 40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
41 41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
42 42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
43 43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
44 44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
45 45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
46 46 end
47 47
48 48 # Permissions
49 49 Redmine::AccessControl.map do |map|
50 50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
51 51 map.permission :search_project, {:search => :index}, :public => true
52 52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
53 53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
54 54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
55 55 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
56 56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
57 57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
58 58
59 59 map.project_module :issue_tracking do |map|
60 60 # Issue categories
61 61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
62 62 # Issues
63 63 map.permission :view_issues, {:issues => [:index, :show],
64 64 :auto_complete => [:issues],
65 65 :context_menus => [:issues],
66 66 :versions => [:index, :show, :status_by],
67 67 :journals => [:index, :diff],
68 68 :queries => :index,
69 69 :reports => [:issue_report, :issue_report_details]}
70 70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
71 71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
72 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
72 map.permission :manage_issue_relations, {:issue_relations => [:show, :create, :destroy]}
73 73 map.permission :manage_subtasks, {}
74 74 map.permission :set_issues_private, {}
75 75 map.permission :set_own_issues_private, {}, :require => :loggedin
76 76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
77 77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
78 78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
79 79 map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
80 80 map.permission :delete_issues, {:issues => :destroy}, :require => :member
81 81 # Queries
82 82 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
83 83 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
84 84 # Watchers
85 85 map.permission :view_issue_watchers, {}
86 86 map.permission :add_issue_watchers, {:watchers => :new}
87 87 map.permission :delete_issue_watchers, {:watchers => :destroy}
88 88 end
89 89
90 90 map.project_module :time_tracking do |map|
91 91 map.permission :log_time, {:timelog => [:new, :create, :edit, :update, :bulk_edit, :bulk_update]}, :require => :loggedin
92 92 map.permission :view_time_entries, :timelog => [:index, :show], :time_entry_reports => [:report]
93 93 map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
94 94 map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
95 95 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
96 96 end
97 97
98 98 map.project_module :news do |map|
99 99 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
100 100 map.permission :view_news, {:news => [:index, :show]}, :public => true
101 101 map.permission :comment_news, {:comments => :create}
102 102 end
103 103
104 104 map.project_module :documents do |map|
105 105 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
106 106 map.permission :view_documents, :documents => [:index, :show, :download]
107 107 end
108 108
109 109 map.project_module :files do |map|
110 110 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
111 111 map.permission :view_files, :files => :index, :versions => :download
112 112 end
113 113
114 114 map.project_module :wiki do |map|
115 115 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
116 116 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
117 117 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
118 118 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
119 119 map.permission :export_wiki_pages, :wiki => [:export]
120 120 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
121 121 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
122 122 map.permission :delete_wiki_pages_attachments, {}
123 123 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
124 124 end
125 125
126 126 map.project_module :repository do |map|
127 127 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
128 128 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
129 129 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
130 130 map.permission :commit_access, {}
131 131 end
132 132
133 133 map.project_module :boards do |map|
134 134 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
135 135 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
136 136 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
137 137 map.permission :edit_messages, {:messages => :edit}, :require => :member
138 138 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
139 139 map.permission :delete_messages, {:messages => :destroy}, :require => :member
140 140 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
141 141 end
142 142
143 143 map.project_module :calendar do |map|
144 144 map.permission :view_calendar, :calendars => [:show, :update]
145 145 end
146 146
147 147 map.project_module :gantt do |map|
148 148 map.permission :view_gantt, :gantts => [:show, :update]
149 149 end
150 150 end
151 151
152 152 Redmine::MenuManager.map :top_menu do |menu|
153 153 menu.push :home, :home_path
154 154 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
155 155 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
156 156 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
157 157 menu.push :help, Redmine::Info.help_url, :last => true
158 158 end
159 159
160 160 Redmine::MenuManager.map :account_menu do |menu|
161 161 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
162 162 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
163 163 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
164 164 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
165 165 end
166 166
167 167 Redmine::MenuManager.map :application_menu do |menu|
168 168 # Empty
169 169 end
170 170
171 171 Redmine::MenuManager.map :admin_menu do |menu|
172 172 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
173 173 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
174 174 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
175 175 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
176 176 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
177 177 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
178 178 :html => {:class => 'issue_statuses'}
179 179 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
180 180 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
181 181 :html => {:class => 'custom_fields'}
182 182 menu.push :enumerations, {:controller => 'enumerations'}
183 183 menu.push :settings, {:controller => 'settings'}
184 184 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
185 185 :html => {:class => 'server_authentication'}
186 186 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
187 187 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
188 188 end
189 189
190 190 Redmine::MenuManager.map :project_menu do |menu|
191 191 menu.push :overview, { :controller => 'projects', :action => 'show' }
192 192 menu.push :activity, { :controller => 'activities', :action => 'index' }
193 193 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
194 194 :if => Proc.new { |p| p.shared_versions.any? }
195 195 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
196 196 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
197 197 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
198 198 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
199 199 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
200 200 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
201 201 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
202 202 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
203 203 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
204 204 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
205 205 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
206 206 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
207 207 menu.push :repository, { :controller => 'repositories', :action => 'show' },
208 208 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
209 209 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
210 210 end
211 211
212 212 Redmine::Activity.map do |activity|
213 213 activity.register :issues, :class_name => %w(Issue Journal)
214 214 activity.register :changesets
215 215 activity.register :news
216 216 activity.register :documents, :class_name => %w(Document Attachment)
217 217 activity.register :files, :class_name => 'Attachment'
218 218 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
219 219 activity.register :messages, :default => false
220 220 activity.register :time_entries, :default => false
221 221 end
222 222
223 223 Redmine::Search.map do |search|
224 224 search.register :issues
225 225 search.register :news
226 226 search.register :documents
227 227 search.register :changesets
228 228 search.register :wiki_pages
229 229 search.register :messages
230 230 search.register :projects
231 231 end
232 232
233 233 Redmine::WikiFormatting.map do |format|
234 234 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
235 235 end
236 236
237 237 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,102 +1,119
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
1 18 require File.expand_path('../../test_helper', __FILE__)
2 19 require 'issue_relations_controller'
3 20
4 21 # Re-raise errors caught by the controller.
5 22 class IssueRelationsController; def rescue_action(e) raise e end; end
6 23
7 24
8 25 class IssueRelationsControllerTest < ActionController::TestCase
9 26 fixtures :projects,
10 27 :users,
11 28 :roles,
12 29 :members,
13 30 :member_roles,
14 31 :issues,
15 32 :issue_statuses,
16 33 :issue_relations,
17 34 :enabled_modules,
18 35 :enumerations,
19 36 :trackers
20 37
21 38 def setup
22 39 @controller = IssueRelationsController.new
23 40 @request = ActionController::TestRequest.new
24 41 @response = ActionController::TestResponse.new
25 42 User.current = nil
26 43 end
27 44
28 def test_new
45 def test_create
29 46 assert_difference 'IssueRelation.count' do
30 47 @request.session[:user_id] = 3
31 post :new, :issue_id => 1,
48 post :create, :issue_id => 1,
32 49 :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''}
33 50 end
34 51 end
35 52
36 def test_new_xhr
53 def test_create_xhr
37 54 assert_difference 'IssueRelation.count' do
38 55 @request.session[:user_id] = 3
39 xhr :post, :new,
56 xhr :post, :create,
40 57 :issue_id => 3,
41 58 :relation => {:issue_to_id => '1', :relation_type => 'relates', :delay => ''}
42 59 assert_select_rjs 'relations' do
43 60 assert_select 'table', 1
44 61 assert_select 'tr', 2 # relations
45 62 end
46 63 end
47 64 end
48 65
49 def test_new_should_accept_id_with_hash
66 def test_create_should_accept_id_with_hash
50 67 assert_difference 'IssueRelation.count' do
51 68 @request.session[:user_id] = 3
52 post :new, :issue_id => 1,
69 post :create, :issue_id => 1,
53 70 :relation => {:issue_to_id => '#2', :relation_type => 'relates', :delay => ''}
54 71 end
55 72 end
56 73
57 def test_new_should_not_break_with_non_numerical_id
74 def test_create_should_not_break_with_non_numerical_id
58 75 assert_no_difference 'IssueRelation.count' do
59 76 assert_nothing_raised do
60 77 @request.session[:user_id] = 3
61 post :new, :issue_id => 1,
78 post :create, :issue_id => 1,
62 79 :relation => {:issue_to_id => 'foo', :relation_type => 'relates', :delay => ''}
63 80 end
64 81 end
65 82 end
66 83
67 84 def test_should_create_relations_with_visible_issues_only
68 85 Setting.cross_project_issue_relations = '1'
69 86 assert_nil Issue.visible(User.find(3)).find_by_id(4)
70 87
71 88 assert_no_difference 'IssueRelation.count' do
72 89 @request.session[:user_id] = 3
73 post :new, :issue_id => 1,
90 post :create, :issue_id => 1,
74 91 :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''}
75 92 end
76 93 end
77 94
78 95 should "prevent relation creation when there's a circular dependency"
79 96
80 97 def test_destroy
81 98 assert_difference 'IssueRelation.count', -1 do
82 99 @request.session[:user_id] = 3
83 post :destroy, :id => '2', :issue_id => '3'
100 delete :destroy, :id => '2', :issue_id => '3'
84 101 end
85 102 end
86 103
87 104 def test_destroy_xhr
88 105 IssueRelation.create!(:relation_type => IssueRelation::TYPE_RELATES) do |r|
89 106 r.issue_from_id = 3
90 107 r.issue_to_id = 1
91 108 end
92 109
93 110 assert_difference 'IssueRelation.count', -1 do
94 111 @request.session[:user_id] = 3
95 xhr :post, :destroy, :id => '2', :issue_id => '3'
112 xhr :delete, :destroy, :id => '2', :issue_id => '3'
96 113 assert_select_rjs 'relations' do
97 114 assert_select 'table', 1
98 115 assert_select 'tr', 1 # relation left
99 116 end
100 117 end
101 118 end
102 119 end
@@ -1,362 +1,371
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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RoutingTest < ActionController::IntegrationTest
21 21 context "activities" do
22 22 should_route :get, "/activity", :controller => 'activities', :action => 'index', :id => nil
23 23 should_route :get, "/activity.atom", :controller => 'activities', :action => 'index', :id => nil, :format => 'atom'
24 24 end
25 25
26 26 context "attachments" do
27 27 should_route :get, "/attachments/1", :controller => 'attachments', :action => 'show', :id => '1'
28 28 should_route :get, "/attachments/1/filename.ext", :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'
29 29 should_route :get, "/attachments/download/1", :controller => 'attachments', :action => 'download', :id => '1'
30 30 should_route :get, "/attachments/download/1/filename.ext", :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'
31 31 end
32 32
33 33 context "boards" do
34 34 should_route :get, "/projects/world_domination/boards", :controller => 'boards', :action => 'index', :project_id => 'world_domination'
35 35 should_route :get, "/projects/world_domination/boards/new", :controller => 'boards', :action => 'new', :project_id => 'world_domination'
36 36 should_route :get, "/projects/world_domination/boards/44", :controller => 'boards', :action => 'show', :project_id => 'world_domination', :id => '44'
37 37 should_route :get, "/projects/world_domination/boards/44.atom", :controller => 'boards', :action => 'show', :project_id => 'world_domination', :id => '44', :format => 'atom'
38 38 should_route :get, "/projects/world_domination/boards/44/edit", :controller => 'boards', :action => 'edit', :project_id => 'world_domination', :id => '44'
39 39
40 40 should_route :post, "/projects/world_domination/boards/new", :controller => 'boards', :action => 'new', :project_id => 'world_domination'
41 41 should_route :post, "/projects/world_domination/boards/44/edit", :controller => 'boards', :action => 'edit', :project_id => 'world_domination', :id => '44'
42 42 should_route :post, "/projects/world_domination/boards/44/destroy", :controller => 'boards', :action => 'destroy', :project_id => 'world_domination', :id => '44'
43 43
44 44 end
45 45
46 46 context "documents" do
47 47 should_route :get, "/projects/567/documents", :controller => 'documents', :action => 'index', :project_id => '567'
48 48 should_route :get, "/projects/567/documents/new", :controller => 'documents', :action => 'new', :project_id => '567'
49 49 should_route :get, "/documents/22", :controller => 'documents', :action => 'show', :id => '22'
50 50 should_route :get, "/documents/22/edit", :controller => 'documents', :action => 'edit', :id => '22'
51 51
52 52 should_route :post, "/projects/567/documents/new", :controller => 'documents', :action => 'new', :project_id => '567'
53 53 should_route :post, "/documents/567/edit", :controller => 'documents', :action => 'edit', :id => '567'
54 54 should_route :post, "/documents/567/destroy", :controller => 'documents', :action => 'destroy', :id => '567'
55 55 end
56 56
57 57 context "issues" do
58 58 # REST actions
59 59 should_route :get, "/issues", :controller => 'issues', :action => 'index'
60 60 should_route :get, "/issues.pdf", :controller => 'issues', :action => 'index', :format => 'pdf'
61 61 should_route :get, "/issues.atom", :controller => 'issues', :action => 'index', :format => 'atom'
62 62 should_route :get, "/issues.xml", :controller => 'issues', :action => 'index', :format => 'xml'
63 63 should_route :get, "/projects/23/issues", :controller => 'issues', :action => 'index', :project_id => '23'
64 64 should_route :get, "/projects/23/issues.pdf", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
65 65 should_route :get, "/projects/23/issues.atom", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
66 66 should_route :get, "/projects/23/issues.xml", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'xml'
67 67 should_route :get, "/issues/64", :controller => 'issues', :action => 'show', :id => '64'
68 68 should_route :get, "/issues/64.pdf", :controller => 'issues', :action => 'show', :id => '64', :format => 'pdf'
69 69 should_route :get, "/issues/64.atom", :controller => 'issues', :action => 'show', :id => '64', :format => 'atom'
70 70 should_route :get, "/issues/64.xml", :controller => 'issues', :action => 'show', :id => '64', :format => 'xml'
71 71
72 72 should_route :get, "/projects/23/issues/new", :controller => 'issues', :action => 'new', :project_id => '23'
73 73 should_route :post, "/projects/23/issues", :controller => 'issues', :action => 'create', :project_id => '23'
74 74 should_route :post, "/issues.xml", :controller => 'issues', :action => 'create', :format => 'xml'
75 75
76 76 should_route :get, "/issues/64/edit", :controller => 'issues', :action => 'edit', :id => '64'
77 77 # TODO: Should use PUT
78 78 should_route :post, "/issues/64/edit", :controller => 'issues', :action => 'edit', :id => '64'
79 79 should_route :put, "/issues/1.xml", :controller => 'issues', :action => 'update', :id => '1', :format => 'xml'
80 80
81 81 # TODO: Should use DELETE
82 82 should_route :post, "/issues/64/destroy", :controller => 'issues', :action => 'destroy', :id => '64'
83 83 should_route :delete, "/issues/1.xml", :controller => 'issues', :action => 'destroy', :id => '1', :format => 'xml'
84 84
85 85 # Extra actions
86 86 should_route :get, "/projects/23/issues/64/copy", :controller => 'issues', :action => 'new', :project_id => '23', :copy_from => '64'
87 87
88 88 should_route :get, "/issues/move/new", :controller => 'issue_moves', :action => 'new'
89 89 should_route :post, "/issues/move", :controller => 'issue_moves', :action => 'create'
90 90
91 91 should_route :post, "/issues/1/quoted", :controller => 'journals', :action => 'new', :id => '1'
92 92
93 93 should_route :get, "/issues/calendar", :controller => 'calendars', :action => 'show'
94 94 should_route :get, "/projects/project-name/issues/calendar", :controller => 'calendars', :action => 'show', :project_id => 'project-name'
95 95
96 96 should_route :get, "/issues/gantt", :controller => 'gantts', :action => 'show'
97 97 should_route :get, "/issues/gantt.pdf", :controller => 'gantts', :action => 'show', :format => 'pdf'
98 98 should_route :get, "/projects/project-name/issues/gantt", :controller => 'gantts', :action => 'show', :project_id => 'project-name'
99 99 should_route :get, "/projects/project-name/issues/gantt.pdf", :controller => 'gantts', :action => 'show', :project_id => 'project-name', :format => 'pdf'
100 100
101 101 should_route :get, "/issues/auto_complete", :controller => 'auto_completes', :action => 'issues'
102 102
103 103 should_route :get, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123'
104 104 should_route :post, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123'
105 105 should_route :get, "/issues/context_menu", :controller => 'context_menus', :action => 'issues'
106 106 should_route :post, "/issues/context_menu", :controller => 'context_menus', :action => 'issues'
107 107
108 108 should_route :get, "/issues/changes", :controller => 'journals', :action => 'index'
109 109
110 110 should_route :get, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_edit'
111 111 should_route :post, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_update'
112 112 end
113 113
114 114 context "issue categories" do
115 115 should_route :get, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test'
116 116
117 117 should_route :post, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test'
118 118 end
119 119
120 120 context "issue relations" do
121 should_route :post, "/issues/1/relations", :controller => 'issue_relations', :action => 'new', :issue_id => '1'
122 should_route :post, "/issues/1/relations/23/destroy", :controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23'
121 should_route :post, "/issues/1/relations", :controller => 'issue_relations', :action => 'create', :issue_id => '1'
122 should_route :post, "/issues/1/relations.xml", :controller => 'issue_relations', :action => 'create', :issue_id => '1', :format => 'xml'
123 should_route :post, "/issues/1/relations.json", :controller => 'issue_relations', :action => 'create', :issue_id => '1', :format => 'json'
124
125 should_route :get, "/issues/1/relations/23", :controller => 'issue_relations', :action => 'show', :issue_id => '1', :id => '23'
126 should_route :get, "/issues/1/relations/23.xml", :controller => 'issue_relations', :action => 'show', :issue_id => '1', :id => '23', :format => 'xml'
127 should_route :get, "/issues/1/relations/23.json", :controller => 'issue_relations', :action => 'show', :issue_id => '1', :id => '23', :format => 'json'
128
129 should_route :delete, "/issues/1/relations/23", :controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23'
130 should_route :delete, "/issues/1/relations/23.xml", :controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23', :format => 'xml'
131 should_route :delete, "/issues/1/relations/23.json", :controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23', :format => 'json'
123 132 end
124 133
125 134 context "issue reports" do
126 135 should_route :get, "/projects/567/issues/report", :controller => 'reports', :action => 'issue_report', :id => '567'
127 136 should_route :get, "/projects/567/issues/report/assigned_to", :controller => 'reports', :action => 'issue_report_details', :id => '567', :detail => 'assigned_to'
128 137 end
129 138
130 139 context "members" do
131 140 should_route :post, "/projects/5234/members/new", :controller => 'members', :action => 'new', :id => '5234'
132 141 end
133 142
134 143 context "messages" do
135 144 should_route :get, "/boards/22/topics/2", :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
136 145 should_route :get, "/boards/lala/topics/new", :controller => 'messages', :action => 'new', :board_id => 'lala'
137 146 should_route :get, "/boards/lala/topics/22/edit", :controller => 'messages', :action => 'edit', :id => '22', :board_id => 'lala'
138 147
139 148 should_route :post, "/boards/lala/topics/new", :controller => 'messages', :action => 'new', :board_id => 'lala'
140 149 should_route :post, "/boards/lala/topics/22/edit", :controller => 'messages', :action => 'edit', :id => '22', :board_id => 'lala'
141 150 should_route :post, "/boards/22/topics/555/replies", :controller => 'messages', :action => 'reply', :id => '555', :board_id => '22'
142 151 should_route :post, "/boards/22/topics/555/destroy", :controller => 'messages', :action => 'destroy', :id => '555', :board_id => '22'
143 152 end
144 153
145 154 context "news" do
146 155 should_route :get, "/news", :controller => 'news', :action => 'index'
147 156 should_route :get, "/news.atom", :controller => 'news', :action => 'index', :format => 'atom'
148 157 should_route :get, "/news.xml", :controller => 'news', :action => 'index', :format => 'xml'
149 158 should_route :get, "/news.json", :controller => 'news', :action => 'index', :format => 'json'
150 159 should_route :get, "/projects/567/news", :controller => 'news', :action => 'index', :project_id => '567'
151 160 should_route :get, "/projects/567/news.atom", :controller => 'news', :action => 'index', :format => 'atom', :project_id => '567'
152 161 should_route :get, "/projects/567/news.xml", :controller => 'news', :action => 'index', :format => 'xml', :project_id => '567'
153 162 should_route :get, "/projects/567/news.json", :controller => 'news', :action => 'index', :format => 'json', :project_id => '567'
154 163 should_route :get, "/news/2", :controller => 'news', :action => 'show', :id => '2'
155 164 should_route :get, "/projects/567/news/new", :controller => 'news', :action => 'new', :project_id => '567'
156 165 should_route :get, "/news/234", :controller => 'news', :action => 'show', :id => '234'
157 166 should_route :get, "/news/567/edit", :controller => 'news', :action => 'edit', :id => '567'
158 167 should_route :get, "/news/preview", :controller => 'previews', :action => 'news'
159 168
160 169 should_route :post, "/projects/567/news", :controller => 'news', :action => 'create', :project_id => '567'
161 170 should_route :post, "/news/567/comments", :controller => 'comments', :action => 'create', :id => '567'
162 171
163 172 should_route :put, "/news/567", :controller => 'news', :action => 'update', :id => '567'
164 173
165 174 should_route :delete, "/news/567", :controller => 'news', :action => 'destroy', :id => '567'
166 175 should_route :delete, "/news/567/comments/15", :controller => 'comments', :action => 'destroy', :id => '567', :comment_id => '15'
167 176 end
168 177
169 178 context "projects" do
170 179 should_route :get, "/projects", :controller => 'projects', :action => 'index'
171 180 should_route :get, "/projects.atom", :controller => 'projects', :action => 'index', :format => 'atom'
172 181 should_route :get, "/projects.xml", :controller => 'projects', :action => 'index', :format => 'xml'
173 182 should_route :get, "/projects/new", :controller => 'projects', :action => 'new'
174 183 should_route :get, "/projects/test", :controller => 'projects', :action => 'show', :id => 'test'
175 184 should_route :get, "/projects/1.xml", :controller => 'projects', :action => 'show', :id => '1', :format => 'xml'
176 185 should_route :get, "/projects/4223/settings", :controller => 'projects', :action => 'settings', :id => '4223'
177 186 should_route :get, "/projects/4223/settings/members", :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
178 187 should_route :get, "/projects/33/files", :controller => 'files', :action => 'index', :project_id => '33'
179 188 should_route :get, "/projects/33/files/new", :controller => 'files', :action => 'new', :project_id => '33'
180 189 should_route :get, "/projects/33/roadmap", :controller => 'versions', :action => 'index', :project_id => '33'
181 190 should_route :get, "/projects/33/activity", :controller => 'activities', :action => 'index', :id => '33'
182 191 should_route :get, "/projects/33/activity.atom", :controller => 'activities', :action => 'index', :id => '33', :format => 'atom'
183 192
184 193 should_route :post, "/projects", :controller => 'projects', :action => 'create'
185 194 should_route :post, "/projects.xml", :controller => 'projects', :action => 'create', :format => 'xml'
186 195 should_route :post, "/projects/33/files", :controller => 'files', :action => 'create', :project_id => '33'
187 196 should_route :post, "/projects/64/archive", :controller => 'projects', :action => 'archive', :id => '64'
188 197 should_route :post, "/projects/64/unarchive", :controller => 'projects', :action => 'unarchive', :id => '64'
189 198
190 199 should_route :put, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'update', :project_id => '64'
191 200 should_route :put, "/projects/4223", :controller => 'projects', :action => 'update', :id => '4223'
192 201 should_route :put, "/projects/1.xml", :controller => 'projects', :action => 'update', :id => '1', :format => 'xml'
193 202
194 203 should_route :delete, "/projects/64", :controller => 'projects', :action => 'destroy', :id => '64'
195 204 should_route :delete, "/projects/1.xml", :controller => 'projects', :action => 'destroy', :id => '1', :format => 'xml'
196 205 should_route :delete, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'destroy', :project_id => '64'
197 206 end
198 207
199 208 context "queries" do
200 209 should_route :get, "/queries/new", :controller => 'queries', :action => 'new'
201 210 should_route :get, "/projects/redmine/queries/new", :controller => 'queries', :action => 'new', :project_id => 'redmine'
202 211
203 212 should_route :post, "/queries/new", :controller => 'queries', :action => 'new'
204 213 should_route :post, "/projects/redmine/queries/new", :controller => 'queries', :action => 'new', :project_id => 'redmine'
205 214 end
206 215
207 216 context "repositories" do
208 217 should_route :get, "/projects/redmine/repository", :controller => 'repositories', :action => 'show', :id => 'redmine'
209 218 should_route :get, "/projects/redmine/repository/edit", :controller => 'repositories', :action => 'edit', :id => 'redmine'
210 219 should_route :get, "/projects/redmine/repository/revisions", :controller => 'repositories', :action => 'revisions', :id => 'redmine'
211 220 should_route :get, "/projects/redmine/repository/revisions.atom", :controller => 'repositories', :action => 'revisions', :id => 'redmine', :format => 'atom'
212 221 should_route :get, "/projects/redmine/repository/revisions/2457", :controller => 'repositories', :action => 'revision', :id => 'redmine', :rev => '2457'
213 222 should_route :get, "/projects/redmine/repository/revisions/2457/diff", :controller => 'repositories', :action => 'diff', :id => 'redmine', :rev => '2457'
214 223 should_route :get, "/projects/redmine/repository/revisions/2457/diff.diff", :controller => 'repositories', :action => 'diff', :id => 'redmine', :rev => '2457', :format => 'diff'
215 224 should_route :get, "/projects/redmine/repository/diff/path/to/file.c", :controller => 'repositories', :action => 'diff', :id => 'redmine', :path => %w[path to file.c]
216 225 should_route :get, "/projects/redmine/repository/revisions/2/diff/path/to/file.c", :controller => 'repositories', :action => 'diff', :id => 'redmine', :path => %w[path to file.c], :rev => '2'
217 226 should_route :get, "/projects/redmine/repository/browse/path/to/file.c", :controller => 'repositories', :action => 'browse', :id => 'redmine', :path => %w[path to file.c]
218 227 should_route :get, "/projects/redmine/repository/entry/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c]
219 228 should_route :get, "/projects/redmine/repository/revisions/2/entry/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :rev => '2'
220 229 should_route :get, "/projects/redmine/repository/raw/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :format => 'raw'
221 230 should_route :get, "/projects/redmine/repository/revisions/2/raw/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :rev => '2', :format => 'raw'
222 231 should_route :get, "/projects/redmine/repository/annotate/path/to/file.c", :controller => 'repositories', :action => 'annotate', :id => 'redmine', :path => %w[path to file.c]
223 232 should_route :get, "/projects/redmine/repository/changes/path/to/file.c", :controller => 'repositories', :action => 'changes', :id => 'redmine', :path => %w[path to file.c]
224 233 should_route :get, "/projects/redmine/repository/statistics", :controller => 'repositories', :action => 'stats', :id => 'redmine'
225 234
226 235
227 236 should_route :post, "/projects/redmine/repository/edit", :controller => 'repositories', :action => 'edit', :id => 'redmine'
228 237 end
229 238
230 239 context "timelogs (global)" do
231 240 should_route :get, "/time_entries", :controller => 'timelog', :action => 'index'
232 241 should_route :get, "/time_entries.csv", :controller => 'timelog', :action => 'index', :format => 'csv'
233 242 should_route :get, "/time_entries.atom", :controller => 'timelog', :action => 'index', :format => 'atom'
234 243 should_route :get, "/time_entries/new", :controller => 'timelog', :action => 'new'
235 244 should_route :get, "/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22'
236 245
237 246 should_route :post, "/time_entries", :controller => 'timelog', :action => 'create'
238 247
239 248 should_route :put, "/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22'
240 249
241 250 should_route :delete, "/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55'
242 251 end
243 252
244 253 context "timelogs (scoped under project)" do
245 254 should_route :get, "/projects/567/time_entries", :controller => 'timelog', :action => 'index', :project_id => '567'
246 255 should_route :get, "/projects/567/time_entries.csv", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'csv'
247 256 should_route :get, "/projects/567/time_entries.atom", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'atom'
248 257 should_route :get, "/projects/567/time_entries/new", :controller => 'timelog', :action => 'new', :project_id => '567'
249 258 should_route :get, "/projects/567/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :project_id => '567'
250 259
251 260 should_route :post, "/projects/567/time_entries", :controller => 'timelog', :action => 'create', :project_id => '567'
252 261
253 262 should_route :put, "/projects/567/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :project_id => '567'
254 263
255 264 should_route :delete, "/projects/567/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :project_id => '567'
256 265 end
257 266
258 267 context "timelogs (scoped under issues)" do
259 268 should_route :get, "/issues/234/time_entries", :controller => 'timelog', :action => 'index', :issue_id => '234'
260 269 should_route :get, "/issues/234/time_entries.csv", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'csv'
261 270 should_route :get, "/issues/234/time_entries.atom", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'atom'
262 271 should_route :get, "/issues/234/time_entries/new", :controller => 'timelog', :action => 'new', :issue_id => '234'
263 272 should_route :get, "/issues/234/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :issue_id => '234'
264 273
265 274 should_route :post, "/issues/234/time_entries", :controller => 'timelog', :action => 'create', :issue_id => '234'
266 275
267 276 should_route :put, "/issues/234/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :issue_id => '234'
268 277
269 278 should_route :delete, "/issues/234/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :issue_id => '234'
270 279 end
271 280
272 281 context "timelogs (scoped under project and issues)" do
273 282 should_route :get, "/projects/ecookbook/issues/234/time_entries", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook'
274 283 should_route :get, "/projects/ecookbook/issues/234/time_entries.csv", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook', :format => 'csv'
275 284 should_route :get, "/projects/ecookbook/issues/234/time_entries.atom", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook', :format => 'atom'
276 285 should_route :get, "/projects/ecookbook/issues/234/time_entries/new", :controller => 'timelog', :action => 'new', :issue_id => '234', :project_id => 'ecookbook'
277 286 should_route :get, "/projects/ecookbook/issues/234/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :issue_id => '234', :project_id => 'ecookbook'
278 287
279 288 should_route :post, "/projects/ecookbook/issues/234/time_entries", :controller => 'timelog', :action => 'create', :issue_id => '234', :project_id => 'ecookbook'
280 289
281 290 should_route :put, "/projects/ecookbook/issues/234/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :issue_id => '234', :project_id => 'ecookbook'
282 291
283 292 should_route :delete, "/projects/ecookbook/issues/234/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :issue_id => '234', :project_id => 'ecookbook'
284 293 end
285 294
286 295 context "time_entry_reports" do
287 296 should_route :get, "/time_entries/report", :controller => 'time_entry_reports', :action => 'report'
288 297 should_route :get, "/projects/567/time_entries/report", :controller => 'time_entry_reports', :action => 'report', :project_id => '567'
289 298 should_route :get, "/projects/567/time_entries/report.csv", :controller => 'time_entry_reports', :action => 'report', :project_id => '567', :format => 'csv'
290 299 end
291 300
292 301 context "users" do
293 302 should_route :get, "/users", :controller => 'users', :action => 'index'
294 303 should_route :get, "/users.xml", :controller => 'users', :action => 'index', :format => 'xml'
295 304 should_route :get, "/users/44", :controller => 'users', :action => 'show', :id => '44'
296 305 should_route :get, "/users/44.xml", :controller => 'users', :action => 'show', :id => '44', :format => 'xml'
297 306 should_route :get, "/users/current", :controller => 'users', :action => 'show', :id => 'current'
298 307 should_route :get, "/users/current.xml", :controller => 'users', :action => 'show', :id => 'current', :format => 'xml'
299 308 should_route :get, "/users/new", :controller => 'users', :action => 'new'
300 309 should_route :get, "/users/444/edit", :controller => 'users', :action => 'edit', :id => '444'
301 310 should_route :get, "/users/222/edit/membership", :controller => 'users', :action => 'edit', :id => '222', :tab => 'membership'
302 311
303 312 should_route :post, "/users", :controller => 'users', :action => 'create'
304 313 should_route :post, "/users.xml", :controller => 'users', :action => 'create', :format => 'xml'
305 314 should_route :post, "/users/123/memberships", :controller => 'users', :action => 'edit_membership', :id => '123'
306 315 should_route :post, "/users/123/memberships/55", :controller => 'users', :action => 'edit_membership', :id => '123', :membership_id => '55'
307 316 should_route :post, "/users/567/memberships/12/destroy", :controller => 'users', :action => 'destroy_membership', :id => '567', :membership_id => '12'
308 317
309 318 should_route :put, "/users/444", :controller => 'users', :action => 'update', :id => '444'
310 319 should_route :put, "/users/444.xml", :controller => 'users', :action => 'update', :id => '444', :format => 'xml'
311 320
312 321 should_route :delete, "/users/44", :controller => 'users', :action => 'destroy', :id => '44'
313 322 should_route :delete, "/users/44.xml", :controller => 'users', :action => 'destroy', :id => '44', :format => 'xml'
314 323 end
315 324
316 325 # TODO: should they all be scoped under /projects/:project_id ?
317 326 context "versions" do
318 327 should_route :get, "/projects/foo/versions/new", :controller => 'versions', :action => 'new', :project_id => 'foo'
319 328 should_route :get, "/versions/show/1", :controller => 'versions', :action => 'show', :id => '1'
320 329 should_route :get, "/versions/edit/1", :controller => 'versions', :action => 'edit', :id => '1'
321 330
322 331 should_route :post, "/projects/foo/versions", :controller => 'versions', :action => 'create', :project_id => 'foo'
323 332 should_route :post, "/versions/update/1", :controller => 'versions', :action => 'update', :id => '1'
324 333
325 334 should_route :delete, "/versions/destroy/1", :controller => 'versions', :action => 'destroy', :id => '1'
326 335 end
327 336
328 337 context "wiki (singular, project's pages)" do
329 338 should_route :get, "/projects/567/wiki", :controller => 'wiki', :action => 'show', :project_id => '567'
330 339 should_route :get, "/projects/567/wiki/lalala", :controller => 'wiki', :action => 'show', :project_id => '567', :id => 'lalala'
331 340 should_route :get, "/projects/567/wiki/my_page/edit", :controller => 'wiki', :action => 'edit', :project_id => '567', :id => 'my_page'
332 341 should_route :get, "/projects/1/wiki/CookBook_documentation/history", :controller => 'wiki', :action => 'history', :project_id => '1', :id => 'CookBook_documentation'
333 342 should_route :get, "/projects/1/wiki/CookBook_documentation/diff", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation'
334 343 should_route :get, "/projects/1/wiki/CookBook_documentation/diff/2", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation', :version => '2'
335 344 should_route :get, "/projects/1/wiki/CookBook_documentation/diff/2/vs/1", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation', :version => '2', :version_from => '1'
336 345 should_route :get, "/projects/1/wiki/CookBook_documentation/annotate/2", :controller => 'wiki', :action => 'annotate', :project_id => '1', :id => 'CookBook_documentation', :version => '2'
337 346 should_route :get, "/projects/22/wiki/ladida/rename", :controller => 'wiki', :action => 'rename', :project_id => '22', :id => 'ladida'
338 347 should_route :get, "/projects/567/wiki/index", :controller => 'wiki', :action => 'index', :project_id => '567'
339 348 should_route :get, "/projects/567/wiki/date_index", :controller => 'wiki', :action => 'date_index', :project_id => '567'
340 349 should_route :get, "/projects/567/wiki/export", :controller => 'wiki', :action => 'export', :project_id => '567'
341 350
342 351 should_route :post, "/projects/567/wiki/CookBook_documentation/preview", :controller => 'wiki', :action => 'preview', :project_id => '567', :id => 'CookBook_documentation'
343 352 should_route :post, "/projects/22/wiki/ladida/rename", :controller => 'wiki', :action => 'rename', :project_id => '22', :id => 'ladida'
344 353 should_route :post, "/projects/22/wiki/ladida/protect", :controller => 'wiki', :action => 'protect', :project_id => '22', :id => 'ladida'
345 354 should_route :post, "/projects/22/wiki/ladida/add_attachment", :controller => 'wiki', :action => 'add_attachment', :project_id => '22', :id => 'ladida'
346 355
347 356 should_route :put, "/projects/567/wiki/my_page", :controller => 'wiki', :action => 'update', :project_id => '567', :id => 'my_page'
348 357
349 358 should_route :delete, "/projects/22/wiki/ladida", :controller => 'wiki', :action => 'destroy', :project_id => '22', :id => 'ladida'
350 359 end
351 360
352 361 context "wikis (plural, admin setup)" do
353 362 should_route :get, "/projects/ladida/wiki/destroy", :controller => 'wikis', :action => 'destroy', :id => 'ladida'
354 363
355 364 should_route :post, "/projects/ladida/wiki", :controller => 'wikis', :action => 'edit', :id => 'ladida'
356 365 should_route :post, "/projects/ladida/wiki/destroy", :controller => 'wikis', :action => 'destroy', :id => 'ladida'
357 366 end
358 367
359 368 context "administration panel" do
360 369 should_route :get, "/admin/projects", :controller => 'admin', :action => 'projects'
361 370 end
362 371 end
General Comments 0
You need to be logged in to leave comments. Login now