##// END OF EJS Templates
Adds support for adding attachments to issues through the REST API (#8171)....
Jean-Philippe Lang -
r8808:77626ef6fbf2
parent child
Show More
@@ -0,0 +1,3
1 api.upload do
2 api.token @attachment.token
3 end
@@ -1,100 +1,124
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project, :except => :upload
20 before_filter :file_readable, :read_authorize, :except => :destroy
20 before_filter :file_readable, :read_authorize, :only => [:show, :download]
21 before_filter :delete_authorize, :only => :destroy
21 before_filter :delete_authorize, :only => :destroy
22 before_filter :authorize_global, :only => :upload
22
23
23 accept_api_auth :show, :download
24 accept_api_auth :show, :download, :upload
24
25
25 def show
26 def show
26 respond_to do |format|
27 respond_to do |format|
27 format.html {
28 format.html {
28 if @attachment.is_diff?
29 if @attachment.is_diff?
29 @diff = File.new(@attachment.diskfile, "rb").read
30 @diff = File.new(@attachment.diskfile, "rb").read
30 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
31 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
32 # Save diff type as user preference
33 # Save diff type as user preference
33 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
34 User.current.pref[:diff_type] = @diff_type
35 User.current.pref[:diff_type] = @diff_type
35 User.current.preference.save
36 User.current.preference.save
36 end
37 end
37 render :action => 'diff'
38 render :action => 'diff'
38 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
39 @content = File.new(@attachment.diskfile, "rb").read
40 @content = File.new(@attachment.diskfile, "rb").read
40 render :action => 'file'
41 render :action => 'file'
41 else
42 else
42 download
43 download
43 end
44 end
44 }
45 }
45 format.api
46 format.api
46 end
47 end
47 end
48 end
48
49
49 def download
50 def download
50 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
51 @attachment.increment_download
52 @attachment.increment_download
52 end
53 end
53
54
54 # images are sent inline
55 # images are sent inline
55 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
56 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
56 :type => detect_content_type(@attachment),
57 :type => detect_content_type(@attachment),
57 :disposition => (@attachment.image? ? 'inline' : 'attachment')
58 :disposition => (@attachment.image? ? 'inline' : 'attachment')
58
59
59 end
60 end
60
61
62 def upload
63 # Make sure that API users get used to set this content type
64 # as it won't trigger Rails' automatic parsing of the request body for parameters
65 unless request.content_type == 'application/octet-stream'
66 render :nothing => true, :status => 406
67 return
68 end
69
70 @attachment = Attachment.new(:file => request.body)
71 @attachment.author = User.current
72 @attachment.filename = "test" #ActiveSupport::SecureRandom.hex(16)
73
74 if @attachment.save
75 respond_to do |format|
76 format.api { render :action => 'upload', :status => :created }
77 end
78 else
79 respond_to do |format|
80 format.api { render_validation_errors(@attachment) }
81 end
82 end
83 end
84
61 verify :method => :delete, :only => :destroy
85 verify :method => :delete, :only => :destroy
62 def destroy
86 def destroy
63 # Make sure association callbacks are called
87 # Make sure association callbacks are called
64 @attachment.container.attachments.delete(@attachment)
88 @attachment.container.attachments.delete(@attachment)
65 redirect_to :back
89 redirect_to :back
66 rescue ::ActionController::RedirectBackError
90 rescue ::ActionController::RedirectBackError
67 redirect_to :controller => 'projects', :action => 'show', :id => @project
91 redirect_to :controller => 'projects', :action => 'show', :id => @project
68 end
92 end
69
93
70 private
94 private
71 def find_project
95 def find_project
72 @attachment = Attachment.find(params[:id])
96 @attachment = Attachment.find(params[:id])
73 # Show 404 if the filename in the url is wrong
97 # Show 404 if the filename in the url is wrong
74 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
98 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
75 @project = @attachment.project
99 @project = @attachment.project
76 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
77 render_404
101 render_404
78 end
102 end
79
103
80 # Checks that the file exists and is readable
104 # Checks that the file exists and is readable
81 def file_readable
105 def file_readable
82 @attachment.readable? ? true : render_404
106 @attachment.readable? ? true : render_404
83 end
107 end
84
108
85 def read_authorize
109 def read_authorize
86 @attachment.visible? ? true : deny_access
110 @attachment.visible? ? true : deny_access
87 end
111 end
88
112
89 def delete_authorize
113 def delete_authorize
90 @attachment.deletable? ? true : deny_access
114 @attachment.deletable? ? true : deny_access
91 end
115 end
92
116
93 def detect_content_type(attachment)
117 def detect_content_type(attachment)
94 content_type = attachment.content_type
118 content_type = attachment.content_type
95 if content_type.blank?
119 if content_type.blank?
96 content_type = Redmine::MimeType.of(attachment.filename)
120 content_type = Redmine::MimeType.of(attachment.filename)
97 end
121 end
98 content_type.to_s
122 content_type.to_s
99 end
123 end
100 end
124 end
@@ -1,429 +1,429
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_rss_auth :index, :show
30 accept_rss_auth :index, :show
31 accept_api_auth :index, :show, :create, :update, :destroy
31 accept_api_auth :index, :show, :create, :update, :destroy
32
32
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34
34
35 helper :journals
35 helper :journals
36 helper :projects
36 helper :projects
37 include ProjectsHelper
37 include ProjectsHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :issue_relations
40 helper :issue_relations
41 include IssueRelationsHelper
41 include IssueRelationsHelper
42 helper :watchers
42 helper :watchers
43 include WatchersHelper
43 include WatchersHelper
44 helper :attachments
44 helper :attachments
45 include AttachmentsHelper
45 include AttachmentsHelper
46 helper :queries
46 helper :queries
47 include QueriesHelper
47 include QueriesHelper
48 helper :repositories
48 helper :repositories
49 include RepositoriesHelper
49 include RepositoriesHelper
50 helper :sort
50 helper :sort
51 include SortHelper
51 include SortHelper
52 include IssuesHelper
52 include IssuesHelper
53 helper :timelog
53 helper :timelog
54 helper :gantt
54 helper :gantt
55 include Redmine::Export::PDF
55 include Redmine::Export::PDF
56
56
57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60
60
61 def index
61 def index
62 retrieve_query
62 retrieve_query
63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_update(@query.sortable_columns)
64 sort_update(@query.sortable_columns)
65
65
66 if @query.valid?
66 if @query.valid?
67 case params[:format]
67 case params[:format]
68 when 'csv', 'pdf'
68 when 'csv', 'pdf'
69 @limit = Setting.issues_export_limit.to_i
69 @limit = Setting.issues_export_limit.to_i
70 when 'atom'
70 when 'atom'
71 @limit = Setting.feeds_limit.to_i
71 @limit = Setting.feeds_limit.to_i
72 when 'xml', 'json'
72 when 'xml', 'json'
73 @offset, @limit = api_offset_and_limit
73 @offset, @limit = api_offset_and_limit
74 else
74 else
75 @limit = per_page_option
75 @limit = per_page_option
76 end
76 end
77
77
78 @issue_count = @query.issue_count
78 @issue_count = @query.issue_count
79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
80 @offset ||= @issue_pages.current.offset
80 @offset ||= @issue_pages.current.offset
81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 :order => sort_clause,
82 :order => sort_clause,
83 :offset => @offset,
83 :offset => @offset,
84 :limit => @limit)
84 :limit => @limit)
85 @issue_count_by_group = @query.issue_count_by_group
85 @issue_count_by_group = @query.issue_count_by_group
86
86
87 respond_to do |format|
87 respond_to do |format|
88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
89 format.api {
89 format.api {
90 Issue.load_relations(@issues) if include_in_api_response?('relations')
90 Issue.load_relations(@issues) if include_in_api_response?('relations')
91 }
91 }
92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
95 end
95 end
96 else
96 else
97 respond_to do |format|
97 respond_to do |format|
98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
100 format.api { render_validation_errors(@query) }
100 format.api { render_validation_errors(@query) }
101 end
101 end
102 end
102 end
103 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
104 render_404
104 render_404
105 end
105 end
106
106
107 def show
107 def show
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111
111
112 @changesets = @issue.changesets.visible.all
112 @changesets = @issue.changesets.visible.all
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114
114
115 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
115 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
116 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
117 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
118 @priorities = IssuePriority.active
118 @priorities = IssuePriority.active
119 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
119 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
120 respond_to do |format|
120 respond_to do |format|
121 format.html {
121 format.html {
122 retrieve_previous_and_next_issue_ids
122 retrieve_previous_and_next_issue_ids
123 render :template => 'issues/show'
123 render :template => 'issues/show'
124 }
124 }
125 format.api
125 format.api
126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
128 end
128 end
129 end
129 end
130
130
131 # Add a new issue
131 # Add a new issue
132 # The new issue will be created from an existing one if copy_from parameter is given
132 # The new issue will be created from an existing one if copy_from parameter is given
133 def new
133 def new
134 respond_to do |format|
134 respond_to do |format|
135 format.html { render :action => 'new', :layout => !request.xhr? }
135 format.html { render :action => 'new', :layout => !request.xhr? }
136 format.js {
136 format.js {
137 render(:update) { |page|
137 render(:update) { |page|
138 if params[:project_change]
138 if params[:project_change]
139 page.replace_html 'all_attributes', :partial => 'form'
139 page.replace_html 'all_attributes', :partial => 'form'
140 else
140 else
141 page.replace_html 'attributes', :partial => 'attributes'
141 page.replace_html 'attributes', :partial => 'attributes'
142 end
142 end
143 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
143 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
144 page << "if ($('log_time')) {Element.#{m}('log_time');}"
144 page << "if ($('log_time')) {Element.#{m}('log_time');}"
145 }
145 }
146 }
146 }
147 end
147 end
148 end
148 end
149
149
150 def create
150 def create
151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
152 @issue.save_attachments(params[:attachments])
152 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
153 if @issue.save
153 if @issue.save
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
155 respond_to do |format|
155 respond_to do |format|
156 format.html {
156 format.html {
157 render_attachment_warning_if_needed(@issue)
157 render_attachment_warning_if_needed(@issue)
158 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
158 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
159 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
159 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
160 { :action => 'show', :id => @issue })
160 { :action => 'show', :id => @issue })
161 }
161 }
162 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
162 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
163 end
163 end
164 return
164 return
165 else
165 else
166 respond_to do |format|
166 respond_to do |format|
167 format.html { render :action => 'new' }
167 format.html { render :action => 'new' }
168 format.api { render_validation_errors(@issue) }
168 format.api { render_validation_errors(@issue) }
169 end
169 end
170 end
170 end
171 end
171 end
172
172
173 def edit
173 def edit
174 return unless update_issue_from_params
174 return unless update_issue_from_params
175
175
176 respond_to do |format|
176 respond_to do |format|
177 format.html { }
177 format.html { }
178 format.xml { }
178 format.xml { }
179 end
179 end
180 end
180 end
181
181
182 def update
182 def update
183 return unless update_issue_from_params
183 return unless update_issue_from_params
184 @issue.save_attachments(params[:attachments])
184 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
185 saved = false
185 saved = false
186 begin
186 begin
187 saved = @issue.save_issue_with_child_records(params, @time_entry)
187 saved = @issue.save_issue_with_child_records(params, @time_entry)
188 rescue ActiveRecord::StaleObjectError
188 rescue ActiveRecord::StaleObjectError
189 @conflict = true
189 @conflict = true
190 if params[:last_journal_id]
190 if params[:last_journal_id]
191 if params[:last_journal_id].present?
191 if params[:last_journal_id].present?
192 last_journal_id = params[:last_journal_id].to_i
192 last_journal_id = params[:last_journal_id].to_i
193 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
193 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
194 else
194 else
195 @conflict_journals = @issue.journals.all
195 @conflict_journals = @issue.journals.all
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 if saved
200 if saved
201 render_attachment_warning_if_needed(@issue)
201 render_attachment_warning_if_needed(@issue)
202 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
202 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
203
203
204 respond_to do |format|
204 respond_to do |format|
205 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
205 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
206 format.api { head :ok }
206 format.api { head :ok }
207 end
207 end
208 else
208 else
209 respond_to do |format|
209 respond_to do |format|
210 format.html { render :action => 'edit' }
210 format.html { render :action => 'edit' }
211 format.api { render_validation_errors(@issue) }
211 format.api { render_validation_errors(@issue) }
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 # Bulk edit/copy a set of issues
216 # Bulk edit/copy a set of issues
217 def bulk_edit
217 def bulk_edit
218 @issues.sort!
218 @issues.sort!
219 @copy = params[:copy].present?
219 @copy = params[:copy].present?
220 @notes = params[:notes]
220 @notes = params[:notes]
221
221
222 if User.current.allowed_to?(:move_issues, @projects)
222 if User.current.allowed_to?(:move_issues, @projects)
223 @allowed_projects = Issue.allowed_target_projects_on_move
223 @allowed_projects = Issue.allowed_target_projects_on_move
224 if params[:issue]
224 if params[:issue]
225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
226 if @target_project
226 if @target_project
227 target_projects = [@target_project]
227 target_projects = [@target_project]
228 end
228 end
229 end
229 end
230 end
230 end
231 target_projects ||= @projects
231 target_projects ||= @projects
232
232
233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
234 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
234 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
235 @assignables = target_projects.map(&:assignable_users).reduce(:&)
235 @assignables = target_projects.map(&:assignable_users).reduce(:&)
236 @trackers = target_projects.map(&:trackers).reduce(:&)
236 @trackers = target_projects.map(&:trackers).reduce(:&)
237
237
238 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
238 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
239 render :layout => false if request.xhr?
239 render :layout => false if request.xhr?
240 end
240 end
241
241
242 def bulk_update
242 def bulk_update
243 @issues.sort!
243 @issues.sort!
244 @copy = params[:copy].present?
244 @copy = params[:copy].present?
245 attributes = parse_params_for_bulk_issue_attributes(params)
245 attributes = parse_params_for_bulk_issue_attributes(params)
246
246
247 unsaved_issue_ids = []
247 unsaved_issue_ids = []
248 moved_issues = []
248 moved_issues = []
249 @issues.each do |issue|
249 @issues.each do |issue|
250 issue.reload
250 issue.reload
251 if @copy
251 if @copy
252 issue = issue.copy
252 issue = issue.copy
253 end
253 end
254 journal = issue.init_journal(User.current, params[:notes])
254 journal = issue.init_journal(User.current, params[:notes])
255 issue.safe_attributes = attributes
255 issue.safe_attributes = attributes
256 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
256 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
257 if issue.save
257 if issue.save
258 moved_issues << issue
258 moved_issues << issue
259 else
259 else
260 # Keep unsaved issue ids to display them in flash error
260 # Keep unsaved issue ids to display them in flash error
261 unsaved_issue_ids << issue.id
261 unsaved_issue_ids << issue.id
262 end
262 end
263 end
263 end
264 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
264 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
265
265
266 if params[:follow]
266 if params[:follow]
267 if @issues.size == 1 && moved_issues.size == 1
267 if @issues.size == 1 && moved_issues.size == 1
268 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
268 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
269 elsif moved_issues.map(&:project).uniq.size == 1
269 elsif moved_issues.map(&:project).uniq.size == 1
270 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
270 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
271 end
271 end
272 else
272 else
273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
274 end
274 end
275 end
275 end
276
276
277 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
277 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
278 def destroy
278 def destroy
279 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
279 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
280 if @hours > 0
280 if @hours > 0
281 case params[:todo]
281 case params[:todo]
282 when 'destroy'
282 when 'destroy'
283 # nothing to do
283 # nothing to do
284 when 'nullify'
284 when 'nullify'
285 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
285 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
286 when 'reassign'
286 when 'reassign'
287 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
287 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
288 if reassign_to.nil?
288 if reassign_to.nil?
289 flash.now[:error] = l(:error_issue_not_found_in_project)
289 flash.now[:error] = l(:error_issue_not_found_in_project)
290 return
290 return
291 else
291 else
292 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
292 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
293 end
293 end
294 else
294 else
295 # display the destroy form if it's a user request
295 # display the destroy form if it's a user request
296 return unless api_request?
296 return unless api_request?
297 end
297 end
298 end
298 end
299 @issues.each do |issue|
299 @issues.each do |issue|
300 begin
300 begin
301 issue.reload.destroy
301 issue.reload.destroy
302 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
302 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
303 # nothing to do, issue was already deleted (eg. by a parent)
303 # nothing to do, issue was already deleted (eg. by a parent)
304 end
304 end
305 end
305 end
306 respond_to do |format|
306 respond_to do |format|
307 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
307 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
308 format.api { head :ok }
308 format.api { head :ok }
309 end
309 end
310 end
310 end
311
311
312 private
312 private
313 def find_issue
313 def find_issue
314 # Issue.visible.find(...) can not be used to redirect user to the login form
314 # Issue.visible.find(...) can not be used to redirect user to the login form
315 # if the issue actually exists but requires authentication
315 # if the issue actually exists but requires authentication
316 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
316 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
317 unless @issue.visible?
317 unless @issue.visible?
318 deny_access
318 deny_access
319 return
319 return
320 end
320 end
321 @project = @issue.project
321 @project = @issue.project
322 rescue ActiveRecord::RecordNotFound
322 rescue ActiveRecord::RecordNotFound
323 render_404
323 render_404
324 end
324 end
325
325
326 def find_project
326 def find_project
327 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
327 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
328 @project = Project.find(project_id)
328 @project = Project.find(project_id)
329 rescue ActiveRecord::RecordNotFound
329 rescue ActiveRecord::RecordNotFound
330 render_404
330 render_404
331 end
331 end
332
332
333 def retrieve_previous_and_next_issue_ids
333 def retrieve_previous_and_next_issue_ids
334 retrieve_query_from_session
334 retrieve_query_from_session
335 if @query
335 if @query
336 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
336 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
337 sort_update(@query.sortable_columns, 'issues_index_sort')
337 sort_update(@query.sortable_columns, 'issues_index_sort')
338 limit = 500
338 limit = 500
339 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
339 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
340 if (idx = issue_ids.index(@issue.id)) && idx < limit
340 if (idx = issue_ids.index(@issue.id)) && idx < limit
341 if issue_ids.size < 500
341 if issue_ids.size < 500
342 @issue_position = idx + 1
342 @issue_position = idx + 1
343 @issue_count = issue_ids.size
343 @issue_count = issue_ids.size
344 end
344 end
345 @prev_issue_id = issue_ids[idx - 1] if idx > 0
345 @prev_issue_id = issue_ids[idx - 1] if idx > 0
346 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
346 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
347 end
347 end
348 end
348 end
349 end
349 end
350
350
351 # Used by #edit and #update to set some common instance variables
351 # Used by #edit and #update to set some common instance variables
352 # from the params
352 # from the params
353 # TODO: Refactor, not everything in here is needed by #edit
353 # TODO: Refactor, not everything in here is needed by #edit
354 def update_issue_from_params
354 def update_issue_from_params
355 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
355 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
356 @priorities = IssuePriority.active
356 @priorities = IssuePriority.active
357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
359 @time_entry.attributes = params[:time_entry]
359 @time_entry.attributes = params[:time_entry]
360
360
361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
362 @issue.init_journal(User.current, @notes)
362 @issue.init_journal(User.current, @notes)
363
363
364 issue_attributes = params[:issue]
364 issue_attributes = params[:issue]
365 if issue_attributes && params[:conflict_resolution]
365 if issue_attributes && params[:conflict_resolution]
366 case params[:conflict_resolution]
366 case params[:conflict_resolution]
367 when 'overwrite'
367 when 'overwrite'
368 issue_attributes = issue_attributes.dup
368 issue_attributes = issue_attributes.dup
369 issue_attributes.delete(:lock_version)
369 issue_attributes.delete(:lock_version)
370 when 'add_notes'
370 when 'add_notes'
371 issue_attributes = {}
371 issue_attributes = {}
372 when 'cancel'
372 when 'cancel'
373 redirect_to issue_path(@issue)
373 redirect_to issue_path(@issue)
374 return false
374 return false
375 end
375 end
376 end
376 end
377 @issue.safe_attributes = issue_attributes
377 @issue.safe_attributes = issue_attributes
378 true
378 true
379 end
379 end
380
380
381 # TODO: Refactor, lots of extra code in here
381 # TODO: Refactor, lots of extra code in here
382 # TODO: Changing tracker on an existing issue should not trigger this
382 # TODO: Changing tracker on an existing issue should not trigger this
383 def build_new_issue_from_params
383 def build_new_issue_from_params
384 if params[:id].blank?
384 if params[:id].blank?
385 @issue = Issue.new
385 @issue = Issue.new
386 if params[:copy_from]
386 if params[:copy_from]
387 begin
387 begin
388 @copy_from = Issue.visible.find(params[:copy_from])
388 @copy_from = Issue.visible.find(params[:copy_from])
389 @copy_attachments = params[:copy_attachments].present? || request.get?
389 @copy_attachments = params[:copy_attachments].present? || request.get?
390 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
390 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
391 rescue ActiveRecord::RecordNotFound
391 rescue ActiveRecord::RecordNotFound
392 render_404
392 render_404
393 return
393 return
394 end
394 end
395 end
395 end
396 @issue.project = @project
396 @issue.project = @project
397 else
397 else
398 @issue = @project.issues.visible.find(params[:id])
398 @issue = @project.issues.visible.find(params[:id])
399 end
399 end
400
400
401 @issue.project = @project
401 @issue.project = @project
402 @issue.author = User.current
402 @issue.author = User.current
403 # Tracker must be set before custom field values
403 # Tracker must be set before custom field values
404 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
404 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
405 if @issue.tracker.nil?
405 if @issue.tracker.nil?
406 render_error l(:error_no_tracker_in_project)
406 render_error l(:error_no_tracker_in_project)
407 return false
407 return false
408 end
408 end
409 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
409 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
410 @issue.safe_attributes = params[:issue]
410 @issue.safe_attributes = params[:issue]
411
411
412 @priorities = IssuePriority.active
412 @priorities = IssuePriority.active
413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
414 end
414 end
415
415
416 def check_for_default_issue_status
416 def check_for_default_issue_status
417 if IssueStatus.default.nil?
417 if IssueStatus.default.nil?
418 render_error l(:error_no_default_issue_status)
418 render_error l(:error_no_default_issue_status)
419 return false
419 return false
420 end
420 end
421 end
421 end
422
422
423 def parse_params_for_bulk_issue_attributes(params)
423 def parse_params_for_bulk_issue_attributes(params)
424 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
424 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
425 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
425 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
426 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
426 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
427 attributes
427 attributes
428 end
428 end
429 end
429 end
@@ -1,232 +1,243
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :filename, :author
24 validates_presence_of :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validate :validate_max_file_size
27 validate :validate_max_file_size
28
28
29 acts_as_event :title => :filename,
29 acts_as_event :title => :filename,
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31
31
32 acts_as_activity_provider :type => 'files',
32 acts_as_activity_provider :type => 'files',
33 :permission => :view_files,
33 :permission => :view_files,
34 :author_key => :author_id,
34 :author_key => :author_id,
35 :find_options => {:select => "#{Attachment.table_name}.*",
35 :find_options => {:select => "#{Attachment.table_name}.*",
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
38
38
39 acts_as_activity_provider :type => 'documents',
39 acts_as_activity_provider :type => 'documents',
40 :permission => :view_documents,
40 :permission => :view_documents,
41 :author_key => :author_id,
41 :author_key => :author_id,
42 :find_options => {:select => "#{Attachment.table_name}.*",
42 :find_options => {:select => "#{Attachment.table_name}.*",
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45
45
46 cattr_accessor :storage_path
46 cattr_accessor :storage_path
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
48
48
49 before_save :files_to_final_location
49 before_save :files_to_final_location
50 after_destroy :delete_from_disk
50 after_destroy :delete_from_disk
51
51
52 def container_with_blank_type_check
52 def container_with_blank_type_check
53 if container_type.blank?
53 if container_type.blank?
54 nil
54 nil
55 else
55 else
56 container_without_blank_type_check
56 container_without_blank_type_check
57 end
57 end
58 end
58 end
59 alias_method_chain :container, :blank_type_check unless method_defined?(:container_without_blank_type_check)
59 alias_method_chain :container, :blank_type_check unless method_defined?(:container_without_blank_type_check)
60
60
61 # Returns an unsaved copy of the attachment
61 # Returns an unsaved copy of the attachment
62 def copy(attributes=nil)
62 def copy(attributes=nil)
63 copy = self.class.new
63 copy = self.class.new
64 copy.attributes = self.attributes.dup.except("id", "downloads")
64 copy.attributes = self.attributes.dup.except("id", "downloads")
65 copy.attributes = attributes if attributes
65 copy.attributes = attributes if attributes
66 copy
66 copy
67 end
67 end
68
68
69 def validate_max_file_size
69 def validate_max_file_size
70 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
70 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
71 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
71 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
72 end
72 end
73 end
73 end
74
74
75 def file=(incoming_file)
75 def file=(incoming_file)
76 unless incoming_file.nil?
76 unless incoming_file.nil?
77 @temp_file = incoming_file
77 @temp_file = incoming_file
78 if @temp_file.size > 0
78 if @temp_file.size > 0
79 self.filename = sanitize_filename(@temp_file.original_filename)
79 if @temp_file.respond_to?(:original_filename)
80 self.disk_filename = Attachment.disk_filename(filename)
80 self.filename = @temp_file.original_filename
81 self.content_type = @temp_file.content_type.to_s.chomp
81 end
82 if content_type.blank?
82 if @temp_file.respond_to?(:content_type)
83 self.content_type = @temp_file.content_type.to_s.chomp
84 end
85 if content_type.blank? && filename.present?
83 self.content_type = Redmine::MimeType.of(filename)
86 self.content_type = Redmine::MimeType.of(filename)
84 end
87 end
85 self.filesize = @temp_file.size
88 self.filesize = @temp_file.size
86 end
89 end
87 end
90 end
88 end
91 end
89
92
90 def file
93 def file
91 nil
94 nil
92 end
95 end
93
96
97 def filename=(arg)
98 write_attribute :filename, sanitize_filename(arg.to_s)
99 if new_record? && disk_filename.blank?
100 self.disk_filename = Attachment.disk_filename(filename)
101 end
102 filename
103 end
104
94 # Copies the temporary file to its final location
105 # Copies the temporary file to its final location
95 # and computes its MD5 hash
106 # and computes its MD5 hash
96 def files_to_final_location
107 def files_to_final_location
97 if @temp_file && (@temp_file.size > 0)
108 if @temp_file && (@temp_file.size > 0)
98 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
109 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
99 md5 = Digest::MD5.new
110 md5 = Digest::MD5.new
100 File.open(diskfile, "wb") do |f|
111 File.open(diskfile, "wb") do |f|
101 buffer = ""
112 buffer = ""
102 while (buffer = @temp_file.read(8192))
113 while (buffer = @temp_file.read(8192))
103 f.write(buffer)
114 f.write(buffer)
104 md5.update(buffer)
115 md5.update(buffer)
105 end
116 end
106 end
117 end
107 self.digest = md5.hexdigest
118 self.digest = md5.hexdigest
108 end
119 end
109 @temp_file = nil
120 @temp_file = nil
110 # Don't save the content type if it's longer than the authorized length
121 # Don't save the content type if it's longer than the authorized length
111 if self.content_type && self.content_type.length > 255
122 if self.content_type && self.content_type.length > 255
112 self.content_type = nil
123 self.content_type = nil
113 end
124 end
114 end
125 end
115
126
116 # Deletes the file from the file system if it's not referenced by other attachments
127 # Deletes the file from the file system if it's not referenced by other attachments
117 def delete_from_disk
128 def delete_from_disk
118 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
129 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
119 delete_from_disk!
130 delete_from_disk!
120 end
131 end
121 end
132 end
122
133
123 # Returns file's location on disk
134 # Returns file's location on disk
124 def diskfile
135 def diskfile
125 "#{@@storage_path}/#{self.disk_filename}"
136 "#{@@storage_path}/#{self.disk_filename}"
126 end
137 end
127
138
128 def increment_download
139 def increment_download
129 increment!(:downloads)
140 increment!(:downloads)
130 end
141 end
131
142
132 def project
143 def project
133 container.try(:project)
144 container.try(:project)
134 end
145 end
135
146
136 def visible?(user=User.current)
147 def visible?(user=User.current)
137 container && container.attachments_visible?(user)
148 container && container.attachments_visible?(user)
138 end
149 end
139
150
140 def deletable?(user=User.current)
151 def deletable?(user=User.current)
141 container && container.attachments_deletable?(user)
152 container && container.attachments_deletable?(user)
142 end
153 end
143
154
144 def image?
155 def image?
145 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
156 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
146 end
157 end
147
158
148 def is_text?
159 def is_text?
149 Redmine::MimeType.is_type?('text', filename)
160 Redmine::MimeType.is_type?('text', filename)
150 end
161 end
151
162
152 def is_diff?
163 def is_diff?
153 self.filename =~ /\.(patch|diff)$/i
164 self.filename =~ /\.(patch|diff)$/i
154 end
165 end
155
166
156 # Returns true if the file is readable
167 # Returns true if the file is readable
157 def readable?
168 def readable?
158 File.readable?(diskfile)
169 File.readable?(diskfile)
159 end
170 end
160
171
161 # Returns the attachment token
172 # Returns the attachment token
162 def token
173 def token
163 "#{id}.#{digest}"
174 "#{id}.#{digest}"
164 end
175 end
165
176
166 # Finds an attachment that matches the given token and that has no container
177 # Finds an attachment that matches the given token and that has no container
167 def self.find_by_token(token)
178 def self.find_by_token(token)
168 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
179 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
169 attachment_id, attachment_digest = $1, $2
180 attachment_id, attachment_digest = $1, $2
170 attachment = Attachment.first(:conditions => {:id => attachment_id, :digest => attachment_digest})
181 attachment = Attachment.first(:conditions => {:id => attachment_id, :digest => attachment_digest})
171 if attachment && attachment.container.nil?
182 if attachment && attachment.container.nil?
172 attachment
183 attachment
173 end
184 end
174 end
185 end
175 end
186 end
176
187
177 # Bulk attaches a set of files to an object
188 # Bulk attaches a set of files to an object
178 #
189 #
179 # Returns a Hash of the results:
190 # Returns a Hash of the results:
180 # :files => array of the attached files
191 # :files => array of the attached files
181 # :unsaved => array of the files that could not be attached
192 # :unsaved => array of the files that could not be attached
182 def self.attach_files(obj, attachments)
193 def self.attach_files(obj, attachments)
183 result = obj.save_attachments(attachments, User.current)
194 result = obj.save_attachments(attachments, User.current)
184 obj.attach_saved_attachments
195 obj.attach_saved_attachments
185 result
196 result
186 end
197 end
187
198
188 def self.latest_attach(attachments, filename)
199 def self.latest_attach(attachments, filename)
189 attachments.sort_by(&:created_on).reverse.detect {
200 attachments.sort_by(&:created_on).reverse.detect {
190 |att| att.filename.downcase == filename.downcase
201 |att| att.filename.downcase == filename.downcase
191 }
202 }
192 end
203 end
193
204
194 def self.prune(age=1.day)
205 def self.prune(age=1.day)
195 attachments = Attachment.all(:conditions => ["created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age])
206 attachments = Attachment.all(:conditions => ["created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age])
196 attachments.each(&:destroy)
207 attachments.each(&:destroy)
197 end
208 end
198
209
199 private
210 private
200
211
201 # Physically deletes the file from the file system
212 # Physically deletes the file from the file system
202 def delete_from_disk!
213 def delete_from_disk!
203 if disk_filename.present? && File.exist?(diskfile)
214 if disk_filename.present? && File.exist?(diskfile)
204 File.delete(diskfile)
215 File.delete(diskfile)
205 end
216 end
206 end
217 end
207
218
208 def sanitize_filename(value)
219 def sanitize_filename(value)
209 # get only the filename, not the whole path
220 # get only the filename, not the whole path
210 just_filename = value.gsub(/^.*(\\|\/)/, '')
221 just_filename = value.gsub(/^.*(\\|\/)/, '')
211
222
212 # Finally, replace invalid characters with underscore
223 # Finally, replace invalid characters with underscore
213 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
224 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
214 end
225 end
215
226
216 # Returns an ASCII or hashed filename
227 # Returns an ASCII or hashed filename
217 def self.disk_filename(filename)
228 def self.disk_filename(filename)
218 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
229 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
219 ascii = ''
230 ascii = ''
220 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
231 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
221 ascii = filename
232 ascii = filename
222 else
233 else
223 ascii = Digest::MD5.hexdigest(filename)
234 ascii = Digest::MD5.hexdigest(filename)
224 # keep the extension if any
235 # keep the extension if any
225 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
236 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
226 end
237 end
227 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
238 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
228 timestamp.succ!
239 timestamp.succ!
229 end
240 end
230 "#{timestamp}_#{ascii}"
241 "#{timestamp}_#{ascii}"
231 end
242 end
232 end
243 end
@@ -1,417 +1,419
1 ActionController::Routing::Routes.draw do |map|
1 ActionController::Routing::Routes.draw do |map|
2 # Add your own custom routes here.
2 # Add your own custom routes here.
3 # The priority is based upon order of creation: first created -> highest priority.
3 # The priority is based upon order of creation: first created -> highest priority.
4
4
5 # Here's a sample route:
5 # Here's a sample route:
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 # Keep in mind you can assign values other than :controller and :action
7 # Keep in mind you can assign values other than :controller and :action
8
8
9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
10
10
11 map.signin 'login', :controller => 'account', :action => 'login',
11 map.signin 'login', :controller => 'account', :action => 'login',
12 :conditions => {:method => [:get, :post]}
12 :conditions => {:method => [:get, :post]}
13 map.signout 'logout', :controller => 'account', :action => 'logout',
13 map.signout 'logout', :controller => 'account', :action => 'logout',
14 :conditions => {:method => :get}
14 :conditions => {:method => :get}
15 map.connect 'account/register', :controller => 'account', :action => 'register',
15 map.connect 'account/register', :controller => 'account', :action => 'register',
16 :conditions => {:method => [:get, :post]}
16 :conditions => {:method => [:get, :post]}
17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
18 :conditions => {:method => [:get, :post]}
18 :conditions => {:method => [:get, :post]}
19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
20 :conditions => {:method => :get}
20 :conditions => {:method => :get}
21
21
22 map.connect 'projects/:id/wiki', :controller => 'wikis',
22 map.connect 'projects/:id/wiki', :controller => 'wikis',
23 :action => 'edit', :conditions => {:method => :post}
23 :action => 'edit', :conditions => {:method => :post}
24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
25 :action => 'destroy', :conditions => {:method => [:get, :post]}
25 :action => 'destroy', :conditions => {:method => [:get, :post]}
26
26
27 map.with_options :controller => 'messages' do |messages_routes|
27 map.with_options :controller => 'messages' do |messages_routes|
28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
32 end
32 end
33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
40 end
40 end
41 end
41 end
42
42
43 # Misc issue routes. TODO: move into resources
43 # Misc issue routes. TODO: move into resources
44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
45 :action => 'issues', :conditions => { :method => :get }
45 :action => 'issues', :conditions => { :method => :get }
46 # TODO: would look nicer as /issues/:id/preview
46 # TODO: would look nicer as /issues/:id/preview
47 map.preview_new_issue '/issues/preview/new/:project_id', :controller => 'previews',
47 map.preview_new_issue '/issues/preview/new/:project_id', :controller => 'previews',
48 :action => 'issue'
48 :action => 'issue'
49 map.preview_edit_issue '/issues/preview/edit/:id', :controller => 'previews',
49 map.preview_edit_issue '/issues/preview/edit/:id', :controller => 'previews',
50 :action => 'issue'
50 :action => 'issue'
51 map.issues_context_menu '/issues/context_menu',
51 map.issues_context_menu '/issues/context_menu',
52 :controller => 'context_menus', :action => 'issues'
52 :controller => 'context_menus', :action => 'issues'
53
53
54 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
54 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
55 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
55 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
56 :id => /\d+/, :conditions => { :method => :post }
56 :id => /\d+/, :conditions => { :method => :post }
57
57
58 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
58 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
59 :id => /\d+/, :conditions => { :method => :get }
59 :id => /\d+/, :conditions => { :method => :get }
60 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
60 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
61 :id => /\d+/, :conditions => { :method => [:get, :post] }
61 :id => /\d+/, :conditions => { :method => [:get, :post] }
62
62
63 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
63 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
64 gantts_routes.connect '/projects/:project_id/issues/gantt'
64 gantts_routes.connect '/projects/:project_id/issues/gantt'
65 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
65 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
66 gantts_routes.connect '/issues/gantt.:format'
66 gantts_routes.connect '/issues/gantt.:format'
67 end
67 end
68
68
69 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
69 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
70 calendars_routes.connect '/projects/:project_id/issues/calendar'
70 calendars_routes.connect '/projects/:project_id/issues/calendar'
71 calendars_routes.connect '/issues/calendar'
71 calendars_routes.connect '/issues/calendar'
72 end
72 end
73
73
74 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
74 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
75 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
75 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
76 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
76 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
77 end
77 end
78
78
79 map.connect 'my/account', :controller => 'my', :action => 'account',
79 map.connect 'my/account', :controller => 'my', :action => 'account',
80 :conditions => {:method => [:get, :post]}
80 :conditions => {:method => [:get, :post]}
81 map.connect 'my/page', :controller => 'my', :action => 'page',
81 map.connect 'my/page', :controller => 'my', :action => 'page',
82 :conditions => {:method => :get}
82 :conditions => {:method => :get}
83 # Redirects to my/page
83 # Redirects to my/page
84 map.connect 'my', :controller => 'my', :action => 'index',
84 map.connect 'my', :controller => 'my', :action => 'index',
85 :conditions => {:method => :get}
85 :conditions => {:method => :get}
86 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
86 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
87 :conditions => {:method => :post}
87 :conditions => {:method => :post}
88 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
88 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
89 :conditions => {:method => :post}
89 :conditions => {:method => :post}
90 map.connect 'my/password', :controller => 'my', :action => 'password',
90 map.connect 'my/password', :controller => 'my', :action => 'password',
91 :conditions => {:method => [:get, :post]}
91 :conditions => {:method => [:get, :post]}
92 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
92 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
93 :conditions => {:method => :get}
93 :conditions => {:method => :get}
94 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
94 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
95 :conditions => {:method => :post}
95 :conditions => {:method => :post}
96 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
96 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
97 :conditions => {:method => :post}
97 :conditions => {:method => :post}
98 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
98 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
99 :conditions => {:method => :post}
99 :conditions => {:method => :post}
100
100
101 map.with_options :controller => 'users' do |users|
101 map.with_options :controller => 'users' do |users|
102 users.user_membership 'users/:id/memberships/:membership_id',
102 users.user_membership 'users/:id/memberships/:membership_id',
103 :action => 'edit_membership',
103 :action => 'edit_membership',
104 :conditions => {:method => :put}
104 :conditions => {:method => :put}
105 users.connect 'users/:id/memberships/:membership_id',
105 users.connect 'users/:id/memberships/:membership_id',
106 :action => 'destroy_membership',
106 :action => 'destroy_membership',
107 :conditions => {:method => :delete}
107 :conditions => {:method => :delete}
108 users.user_memberships 'users/:id/memberships',
108 users.user_memberships 'users/:id/memberships',
109 :action => 'edit_membership',
109 :action => 'edit_membership',
110 :conditions => {:method => :post}
110 :conditions => {:method => :post}
111 end
111 end
112 map.resources :users
112 map.resources :users
113
113
114 # For nice "roadmap" in the url for the index action
114 # For nice "roadmap" in the url for the index action
115 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
115 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
116
116
117 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
117 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
118 map.connect 'news/:id/comments', :controller => 'comments',
118 map.connect 'news/:id/comments', :controller => 'comments',
119 :action => 'create', :conditions => {:method => :post}
119 :action => 'create', :conditions => {:method => :post}
120 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
120 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
121 :action => 'destroy', :conditions => {:method => :delete}
121 :action => 'destroy', :conditions => {:method => :delete}
122
122
123 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
123 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
124 :conditions => {:method => :get}
124 :conditions => {:method => :get}
125 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
125 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
126 :conditions => {:method => :post}
126 :conditions => {:method => :post}
127 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
127 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
128 :conditions => {:method => :post}
128 :conditions => {:method => :post}
129 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
129 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
130 :conditions => {:method => :post}
130 :conditions => {:method => :post}
131 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
131 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
132 :conditions => {:method => :post}
132 :conditions => {:method => :post}
133 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
133 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
134 :conditions => {:method => :get}
134 :conditions => {:method => :get}
135
135
136 # TODO: port to be part of the resources route(s)
136 # TODO: port to be part of the resources route(s)
137 map.with_options :conditions => {:method => :get} do |project_views|
137 map.with_options :conditions => {:method => :get} do |project_views|
138 project_views.connect 'projects/:id/settings/:tab',
138 project_views.connect 'projects/:id/settings/:tab',
139 :controller => 'projects', :action => 'settings'
139 :controller => 'projects', :action => 'settings'
140 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
140 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
141 :controller => 'issues', :action => 'new'
141 :controller => 'issues', :action => 'new'
142 end
142 end
143
143
144 map.resources :projects, :member => {
144 map.resources :projects, :member => {
145 :copy => [:get, :post],
145 :copy => [:get, :post],
146 :settings => :get,
146 :settings => :get,
147 :modules => :post,
147 :modules => :post,
148 :archive => :post,
148 :archive => :post,
149 :unarchive => :post
149 :unarchive => :post
150 } do |project|
150 } do |project|
151 project.resource :enumerations, :controller => 'project_enumerations',
151 project.resource :enumerations, :controller => 'project_enumerations',
152 :only => [:update, :destroy]
152 :only => [:update, :destroy]
153 # issue form update
153 # issue form update
154 project.issue_form 'issues/new', :controller => 'issues',
154 project.issue_form 'issues/new', :controller => 'issues',
155 :action => 'new', :conditions => {:method => [:post, :put]}
155 :action => 'new', :conditions => {:method => [:post, :put]}
156 project.resources :issues, :only => [:index, :new, :create] do |issues|
156 project.resources :issues, :only => [:index, :new, :create] do |issues|
157 issues.resources :time_entries, :controller => 'timelog',
157 issues.resources :time_entries, :controller => 'timelog',
158 :collection => {:report => :get}
158 :collection => {:report => :get}
159 end
159 end
160
160
161 project.resources :files, :only => [:index, :new, :create]
161 project.resources :files, :only => [:index, :new, :create]
162 project.resources :versions, :shallow => true,
162 project.resources :versions, :shallow => true,
163 :collection => {:close_completed => :put},
163 :collection => {:close_completed => :put},
164 :member => {:status_by => :post}
164 :member => {:status_by => :post}
165 project.resources :news, :shallow => true
165 project.resources :news, :shallow => true
166 project.resources :time_entries, :controller => 'timelog',
166 project.resources :time_entries, :controller => 'timelog',
167 :collection => {:report => :get}
167 :collection => {:report => :get}
168 project.resources :queries, :only => [:new, :create]
168 project.resources :queries, :only => [:new, :create]
169 project.resources :issue_categories, :shallow => true
169 project.resources :issue_categories, :shallow => true
170 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
170 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
171 project.resources :boards
171 project.resources :boards
172 project.resources :repositories, :shallow => true, :except => [:index, :show],
172 project.resources :repositories, :shallow => true, :except => [:index, :show],
173 :member => {:committers => [:get, :post]}
173 :member => {:committers => [:get, :post]}
174 project.resources :memberships, :shallow => true, :controller => 'members',
174 project.resources :memberships, :shallow => true, :controller => 'members',
175 :only => [:index, :show, :create, :update, :destroy],
175 :only => [:index, :show, :create, :update, :destroy],
176 :collection => {:autocomplete => :get}
176 :collection => {:autocomplete => :get}
177
177
178 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
178 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
179 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
179 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
180 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
180 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
181 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
181 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
182 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
182 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
183 project.resources :wiki, :except => [:new, :create], :member => {
183 project.resources :wiki, :except => [:new, :create], :member => {
184 :rename => [:get, :post],
184 :rename => [:get, :post],
185 :history => :get,
185 :history => :get,
186 :preview => :any,
186 :preview => :any,
187 :protect => :post,
187 :protect => :post,
188 :add_attachment => :post
188 :add_attachment => :post
189 }, :collection => {
189 }, :collection => {
190 :export => :get,
190 :export => :get,
191 :date_index => :get
191 :date_index => :get
192 }
192 }
193 end
193 end
194
194
195 map.connect 'news', :controller => 'news', :action => 'index'
195 map.connect 'news', :controller => 'news', :action => 'index'
196 map.connect 'news.:format', :controller => 'news', :action => 'index'
196 map.connect 'news.:format', :controller => 'news', :action => 'index'
197
197
198 map.resources :queries, :except => [:show]
198 map.resources :queries, :except => [:show]
199 map.resources :issues,
199 map.resources :issues,
200 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
200 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
201 issues.resources :time_entries, :controller => 'timelog',
201 issues.resources :time_entries, :controller => 'timelog',
202 :collection => {:report => :get}
202 :collection => {:report => :get}
203 issues.resources :relations, :shallow => true,
203 issues.resources :relations, :shallow => true,
204 :controller => 'issue_relations',
204 :controller => 'issue_relations',
205 :only => [:index, :show, :create, :destroy]
205 :only => [:index, :show, :create, :destroy]
206 end
206 end
207 # Bulk deletion
207 # Bulk deletion
208 map.connect '/issues', :controller => 'issues', :action => 'destroy',
208 map.connect '/issues', :controller => 'issues', :action => 'destroy',
209 :conditions => {:method => :delete}
209 :conditions => {:method => :delete}
210
210
211 map.connect '/time_entries/destroy',
211 map.connect '/time_entries/destroy',
212 :controller => 'timelog', :action => 'destroy',
212 :controller => 'timelog', :action => 'destroy',
213 :conditions => { :method => :delete }
213 :conditions => { :method => :delete }
214 map.time_entries_context_menu '/time_entries/context_menu',
214 map.time_entries_context_menu '/time_entries/context_menu',
215 :controller => 'context_menus', :action => 'time_entries'
215 :controller => 'context_menus', :action => 'time_entries'
216
216
217 map.resources :time_entries, :controller => 'timelog',
217 map.resources :time_entries, :controller => 'timelog',
218 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
218 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
219
219
220 map.with_options :controller => 'activities', :action => 'index',
220 map.with_options :controller => 'activities', :action => 'index',
221 :conditions => {:method => :get} do |activity|
221 :conditions => {:method => :get} do |activity|
222 activity.connect 'projects/:id/activity'
222 activity.connect 'projects/:id/activity'
223 activity.connect 'projects/:id/activity.:format'
223 activity.connect 'projects/:id/activity.:format'
224 activity.connect 'activity', :id => nil
224 activity.connect 'activity', :id => nil
225 activity.connect 'activity.:format', :id => nil
225 activity.connect 'activity.:format', :id => nil
226 end
226 end
227
227
228 map.with_options :controller => 'repositories' do |repositories|
228 map.with_options :controller => 'repositories' do |repositories|
229 repositories.with_options :conditions => {:method => :get} do |repository_views|
229 repositories.with_options :conditions => {:method => :get} do |repository_views|
230 repository_views.connect 'projects/:id/repository',
230 repository_views.connect 'projects/:id/repository',
231 :action => 'show'
231 :action => 'show'
232
232
233 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
233 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
234 :action => 'stats'
234 :action => 'stats'
235 repository_views.connect 'projects/:id/repository/:repository_id/graph',
235 repository_views.connect 'projects/:id/repository/:repository_id/graph',
236 :action => 'graph'
236 :action => 'graph'
237
237
238 repository_views.connect 'projects/:id/repository/statistics',
238 repository_views.connect 'projects/:id/repository/statistics',
239 :action => 'stats'
239 :action => 'stats'
240 repository_views.connect 'projects/:id/repository/graph',
240 repository_views.connect 'projects/:id/repository/graph',
241 :action => 'graph'
241 :action => 'graph'
242
242
243 repository_views.connect 'projects/:id/repository/:repository_id/revisions',
243 repository_views.connect 'projects/:id/repository/:repository_id/revisions',
244 :action => 'revisions'
244 :action => 'revisions'
245 repository_views.connect 'projects/:id/repository/:repository_id/revisions.:format',
245 repository_views.connect 'projects/:id/repository/:repository_id/revisions.:format',
246 :action => 'revisions'
246 :action => 'revisions'
247 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev',
247 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev',
248 :action => 'revision'
248 :action => 'revision'
249 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues',
249 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues',
250 :action => 'add_related_issue', :conditions => {:method => :post}
250 :action => 'add_related_issue', :conditions => {:method => :post}
251 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id',
251 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id',
252 :action => 'remove_related_issue', :conditions => {:method => :delete}
252 :action => 'remove_related_issue', :conditions => {:method => :delete}
253 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff',
253 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff',
254 :action => 'diff'
254 :action => 'diff'
255 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff.:format',
255 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff.:format',
256 :action => 'diff'
256 :action => 'diff'
257 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/raw/*path',
257 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/raw/*path',
258 :action => 'entry', :format => 'raw'
258 :action => 'entry', :format => 'raw'
259 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/:action/*path',
259 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/:action/*path',
260 :requirements => {
260 :requirements => {
261 :action => /(browse|show|entry|changes|annotate|diff)/,
261 :action => /(browse|show|entry|changes|annotate|diff)/,
262 :rev => /[a-z0-9\.\-_]+/
262 :rev => /[a-z0-9\.\-_]+/
263 }
263 }
264 repository_views.connect 'projects/:id/repository/:repository_id/raw/*path',
264 repository_views.connect 'projects/:id/repository/:repository_id/raw/*path',
265 :action => 'entry', :format => 'raw'
265 :action => 'entry', :format => 'raw'
266 repository_views.connect 'projects/:id/repository/:repository_id/:action/*path',
266 repository_views.connect 'projects/:id/repository/:repository_id/:action/*path',
267 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
267 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
268
268
269 repository_views.connect 'projects/:id/repository/revisions',
269 repository_views.connect 'projects/:id/repository/revisions',
270 :action => 'revisions'
270 :action => 'revisions'
271 repository_views.connect 'projects/:id/repository/revisions.:format',
271 repository_views.connect 'projects/:id/repository/revisions.:format',
272 :action => 'revisions'
272 :action => 'revisions'
273 repository_views.connect 'projects/:id/repository/revisions/:rev',
273 repository_views.connect 'projects/:id/repository/revisions/:rev',
274 :action => 'revision'
274 :action => 'revision'
275 repository_views.connect 'projects/:id/repository/revisions/:rev/issues',
275 repository_views.connect 'projects/:id/repository/revisions/:rev/issues',
276 :action => 'add_related_issue', :conditions => {:method => :post}
276 :action => 'add_related_issue', :conditions => {:method => :post}
277 repository_views.connect 'projects/:id/repository/revisions/:rev/issues/:issue_id',
277 repository_views.connect 'projects/:id/repository/revisions/:rev/issues/:issue_id',
278 :action => 'remove_related_issue', :conditions => {:method => :delete}
278 :action => 'remove_related_issue', :conditions => {:method => :delete}
279 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
279 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
280 :action => 'diff'
280 :action => 'diff'
281 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
281 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
282 :action => 'diff'
282 :action => 'diff'
283 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
283 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
284 :action => 'entry', :format => 'raw'
284 :action => 'entry', :format => 'raw'
285 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
285 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
286 :requirements => {
286 :requirements => {
287 :action => /(browse|show|entry|changes|annotate|diff)/,
287 :action => /(browse|show|entry|changes|annotate|diff)/,
288 :rev => /[a-z0-9\.\-_]+/
288 :rev => /[a-z0-9\.\-_]+/
289 }
289 }
290 repository_views.connect 'projects/:id/repository/raw/*path',
290 repository_views.connect 'projects/:id/repository/raw/*path',
291 :action => 'entry', :format => 'raw'
291 :action => 'entry', :format => 'raw'
292 repository_views.connect 'projects/:id/repository/:action/*path',
292 repository_views.connect 'projects/:id/repository/:action/*path',
293 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
293 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
294
294
295 repository_views.connect 'projects/:id/repository/:repository_id',
295 repository_views.connect 'projects/:id/repository/:repository_id',
296 :action => 'show'
296 :action => 'show'
297 end
297 end
298
298
299 repositories.connect 'projects/:id/repository/revision',
299 repositories.connect 'projects/:id/repository/revision',
300 :action => 'revision',
300 :action => 'revision',
301 :conditions => {:method => [:get, :post]}
301 :conditions => {:method => [:get, :post]}
302 end
302 end
303
303
304 # additional routes for having the file name at the end of url
304 # additional routes for having the file name at the end of url
305 map.connect 'attachments/:id/:filename', :controller => 'attachments',
305 map.connect 'attachments/:id/:filename', :controller => 'attachments',
306 :action => 'show', :id => /\d+/, :filename => /.*/,
306 :action => 'show', :id => /\d+/, :filename => /.*/,
307 :conditions => {:method => :get}
307 :conditions => {:method => :get}
308 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
308 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
309 :action => 'download', :id => /\d+/, :filename => /.*/,
309 :action => 'download', :id => /\d+/, :filename => /.*/,
310 :conditions => {:method => :get}
310 :conditions => {:method => :get}
311 map.connect 'attachments/download/:id', :controller => 'attachments',
311 map.connect 'attachments/download/:id', :controller => 'attachments',
312 :action => 'download', :id => /\d+/,
312 :action => 'download', :id => /\d+/,
313 :conditions => {:method => :get}
313 :conditions => {:method => :get}
314 map.resources :attachments, :only => [:show, :destroy]
314 map.resources :attachments, :only => [:show, :destroy]
315
315
316 map.resources :groups, :member => {:autocomplete_for_user => :get}
316 map.resources :groups, :member => {:autocomplete_for_user => :get}
317 map.group_users 'groups/:id/users', :controller => 'groups',
317 map.group_users 'groups/:id/users', :controller => 'groups',
318 :action => 'add_users', :id => /\d+/,
318 :action => 'add_users', :id => /\d+/,
319 :conditions => {:method => :post}
319 :conditions => {:method => :post}
320 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
320 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
321 :action => 'remove_user', :id => /\d+/,
321 :action => 'remove_user', :id => /\d+/,
322 :conditions => {:method => :delete}
322 :conditions => {:method => :delete}
323 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
323 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
324 :action => 'destroy_membership', :id => /\d+/,
324 :action => 'destroy_membership', :id => /\d+/,
325 :conditions => {:method => :post}
325 :conditions => {:method => :post}
326 map.connect 'groups/edit_membership/:id', :controller => 'groups',
326 map.connect 'groups/edit_membership/:id', :controller => 'groups',
327 :action => 'edit_membership', :id => /\d+/,
327 :action => 'edit_membership', :id => /\d+/,
328 :conditions => {:method => :post}
328 :conditions => {:method => :post}
329
329
330 map.resources :trackers, :except => :show
330 map.resources :trackers, :except => :show
331 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
331 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
332 map.resources :custom_fields, :except => :show
332 map.resources :custom_fields, :except => :show
333 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
333 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
334 map.resources :enumerations, :except => :show
334 map.resources :enumerations, :except => :show
335
335
336 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
336 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
337
337
338 map.connect 'mail_handler', :controller => 'mail_handler',
338 map.connect 'mail_handler', :controller => 'mail_handler',
339 :action => 'index', :conditions => {:method => :post}
339 :action => 'index', :conditions => {:method => :post}
340
340
341 map.connect 'admin', :controller => 'admin', :action => 'index',
341 map.connect 'admin', :controller => 'admin', :action => 'index',
342 :conditions => {:method => :get}
342 :conditions => {:method => :get}
343 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
343 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
344 :conditions => {:method => :get}
344 :conditions => {:method => :get}
345 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
345 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
346 :conditions => {:method => :get}
346 :conditions => {:method => :get}
347 map.connect 'admin/info', :controller => 'admin', :action => 'info',
347 map.connect 'admin/info', :controller => 'admin', :action => 'info',
348 :conditions => {:method => :get}
348 :conditions => {:method => :get}
349 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
349 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
350 :conditions => {:method => :get}
350 :conditions => {:method => :get}
351 map.connect 'admin/default_configuration', :controller => 'admin',
351 map.connect 'admin/default_configuration', :controller => 'admin',
352 :action => 'default_configuration', :conditions => {:method => :post}
352 :action => 'default_configuration', :conditions => {:method => :post}
353
353
354 # Used by AuthSourcesControllerTest
354 # Used by AuthSourcesControllerTest
355 # TODO : refactor *AuthSourcesController to remove these routes
355 # TODO : refactor *AuthSourcesController to remove these routes
356 map.connect 'auth_sources', :controller => 'auth_sources',
356 map.connect 'auth_sources', :controller => 'auth_sources',
357 :action => 'index', :conditions => {:method => :get}
357 :action => 'index', :conditions => {:method => :get}
358 map.connect 'auth_sources/new', :controller => 'auth_sources',
358 map.connect 'auth_sources/new', :controller => 'auth_sources',
359 :action => 'new', :conditions => {:method => :get}
359 :action => 'new', :conditions => {:method => :get}
360 map.connect 'auth_sources/create', :controller => 'auth_sources',
360 map.connect 'auth_sources/create', :controller => 'auth_sources',
361 :action => 'create', :conditions => {:method => :post}
361 :action => 'create', :conditions => {:method => :post}
362 map.connect 'auth_sources/destroy/:id', :controller => 'auth_sources',
362 map.connect 'auth_sources/destroy/:id', :controller => 'auth_sources',
363 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
363 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
364 map.connect 'auth_sources/test_connection/:id', :controller => 'auth_sources',
364 map.connect 'auth_sources/test_connection/:id', :controller => 'auth_sources',
365 :action => 'test_connection', :conditions => {:method => :get}
365 :action => 'test_connection', :conditions => {:method => :get}
366 map.connect 'auth_sources/edit/:id', :controller => 'auth_sources',
366 map.connect 'auth_sources/edit/:id', :controller => 'auth_sources',
367 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
367 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
368 map.connect 'auth_sources/update/:id', :controller => 'auth_sources',
368 map.connect 'auth_sources/update/:id', :controller => 'auth_sources',
369 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
369 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
370
370
371 map.connect 'ldap_auth_sources', :controller => 'ldap_auth_sources',
371 map.connect 'ldap_auth_sources', :controller => 'ldap_auth_sources',
372 :action => 'index', :conditions => {:method => :get}
372 :action => 'index', :conditions => {:method => :get}
373 map.connect 'ldap_auth_sources/new', :controller => 'ldap_auth_sources',
373 map.connect 'ldap_auth_sources/new', :controller => 'ldap_auth_sources',
374 :action => 'new', :conditions => {:method => :get}
374 :action => 'new', :conditions => {:method => :get}
375 map.connect 'ldap_auth_sources/create', :controller => 'ldap_auth_sources',
375 map.connect 'ldap_auth_sources/create', :controller => 'ldap_auth_sources',
376 :action => 'create', :conditions => {:method => :post}
376 :action => 'create', :conditions => {:method => :post}
377 map.connect 'ldap_auth_sources/destroy/:id', :controller => 'ldap_auth_sources',
377 map.connect 'ldap_auth_sources/destroy/:id', :controller => 'ldap_auth_sources',
378 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
378 :action => 'destroy', :id => /\d+/, :conditions => {:method => :post}
379 map.connect 'ldap_auth_sources/test_connection/:id', :controller => 'ldap_auth_sources',
379 map.connect 'ldap_auth_sources/test_connection/:id', :controller => 'ldap_auth_sources',
380 :action => 'test_connection', :conditions => {:method => :get}
380 :action => 'test_connection', :conditions => {:method => :get}
381 map.connect 'ldap_auth_sources/edit/:id', :controller => 'ldap_auth_sources',
381 map.connect 'ldap_auth_sources/edit/:id', :controller => 'ldap_auth_sources',
382 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
382 :action => 'edit', :id => /\d+/, :conditions => {:method => :get}
383 map.connect 'ldap_auth_sources/update/:id', :controller => 'ldap_auth_sources',
383 map.connect 'ldap_auth_sources/update/:id', :controller => 'ldap_auth_sources',
384 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
384 :action => 'update', :id => /\d+/, :conditions => {:method => :post}
385
385
386 map.connect 'workflows', :controller => 'workflows',
386 map.connect 'workflows', :controller => 'workflows',
387 :action => 'index', :conditions => {:method => :get}
387 :action => 'index', :conditions => {:method => :get}
388 map.connect 'workflows/edit', :controller => 'workflows',
388 map.connect 'workflows/edit', :controller => 'workflows',
389 :action => 'edit', :conditions => {:method => [:get, :post]}
389 :action => 'edit', :conditions => {:method => [:get, :post]}
390 map.connect 'workflows/copy', :controller => 'workflows',
390 map.connect 'workflows/copy', :controller => 'workflows',
391 :action => 'copy', :conditions => {:method => [:get, :post]}
391 :action => 'copy', :conditions => {:method => [:get, :post]}
392
392
393 map.connect 'settings', :controller => 'settings',
393 map.connect 'settings', :controller => 'settings',
394 :action => 'index', :conditions => {:method => :get}
394 :action => 'index', :conditions => {:method => :get}
395 map.connect 'settings/edit', :controller => 'settings',
395 map.connect 'settings/edit', :controller => 'settings',
396 :action => 'edit', :conditions => {:method => [:get, :post]}
396 :action => 'edit', :conditions => {:method => [:get, :post]}
397 map.connect 'settings/plugin/:id', :controller => 'settings',
397 map.connect 'settings/plugin/:id', :controller => 'settings',
398 :action => 'plugin', :conditions => {:method => [:get, :post]}
398 :action => 'plugin', :conditions => {:method => [:get, :post]}
399
399
400 map.with_options :controller => 'sys' do |sys|
400 map.with_options :controller => 'sys' do |sys|
401 sys.connect 'sys/projects.:format',
401 sys.connect 'sys/projects.:format',
402 :action => 'projects',
402 :action => 'projects',
403 :conditions => {:method => :get}
403 :conditions => {:method => :get}
404 sys.connect 'sys/projects/:id/repository.:format',
404 sys.connect 'sys/projects/:id/repository.:format',
405 :action => 'create_project_repository',
405 :action => 'create_project_repository',
406 :conditions => {:method => :post}
406 :conditions => {:method => :post}
407 sys.connect 'sys/fetch_changesets',
407 sys.connect 'sys/fetch_changesets',
408 :action => 'fetch_changesets',
408 :action => 'fetch_changesets',
409 :conditions => {:method => :get}
409 :conditions => {:method => :get}
410 end
410 end
411
411
412 map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post}
413
412 map.connect 'robots.txt', :controller => 'welcome',
414 map.connect 'robots.txt', :controller => 'welcome',
413 :action => 'robots', :conditions => {:method => :get}
415 :action => 'robots', :conditions => {:method => :get}
414
416
415 # Used for OpenID
417 # Used for OpenID
416 map.root :controller => 'account', :action => 'login'
418 map.root :controller => 'account', :action => 'login'
417 end
419 end
@@ -1,238 +1,238
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/search'
4 require 'redmine/search'
5 require 'redmine/custom_field_format'
5 require 'redmine/custom_field_format'
6 require 'redmine/mime_type'
6 require 'redmine/mime_type'
7 require 'redmine/core_ext'
7 require 'redmine/core_ext'
8 require 'redmine/themes'
8 require 'redmine/themes'
9 require 'redmine/hook'
9 require 'redmine/hook'
10 require 'redmine/plugin'
10 require 'redmine/plugin'
11 require 'redmine/notifiable'
11 require 'redmine/notifiable'
12 require 'redmine/wiki_formatting'
12 require 'redmine/wiki_formatting'
13 require 'redmine/scm/base'
13 require 'redmine/scm/base'
14
14
15 begin
15 begin
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
17 rescue LoadError
17 rescue LoadError
18 # RMagick is not available
18 # RMagick is not available
19 end
19 end
20
20
21 if RUBY_VERSION < '1.9'
21 if RUBY_VERSION < '1.9'
22 require 'fastercsv'
22 require 'fastercsv'
23 else
23 else
24 require 'csv'
24 require 'csv'
25 FCSV = CSV
25 FCSV = CSV
26 end
26 end
27
27
28 Redmine::Scm::Base.add "Subversion"
28 Redmine::Scm::Base.add "Subversion"
29 Redmine::Scm::Base.add "Darcs"
29 Redmine::Scm::Base.add "Darcs"
30 Redmine::Scm::Base.add "Mercurial"
30 Redmine::Scm::Base.add "Mercurial"
31 Redmine::Scm::Base.add "Cvs"
31 Redmine::Scm::Base.add "Cvs"
32 Redmine::Scm::Base.add "Bazaar"
32 Redmine::Scm::Base.add "Bazaar"
33 Redmine::Scm::Base.add "Git"
33 Redmine::Scm::Base.add "Git"
34 Redmine::Scm::Base.add "Filesystem"
34 Redmine::Scm::Base.add "Filesystem"
35
35
36 Redmine::CustomFieldFormat.map do |fields|
36 Redmine::CustomFieldFormat.map do |fields|
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
46 end
46 end
47
47
48 # Permissions
48 # Permissions
49 Redmine::AccessControl.map do |map|
49 Redmine::AccessControl.map do |map|
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
51 map.permission :search_project, {:search => :index}, :public => true
51 map.permission :search_project, {:search => :index}, :public => true
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
55 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
55 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
58
58
59 map.project_module :issue_tracking do |map|
59 map.project_module :issue_tracking do |map|
60 # Issue categories
60 # Issue categories
61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
62 # Issues
62 # Issues
63 map.permission :view_issues, {:issues => [:index, :show],
63 map.permission :view_issues, {:issues => [:index, :show],
64 :auto_complete => [:issues],
64 :auto_complete => [:issues],
65 :context_menus => [:issues],
65 :context_menus => [:issues],
66 :versions => [:index, :show, :status_by],
66 :versions => [:index, :show, :status_by],
67 :journals => [:index, :diff],
67 :journals => [:index, :diff],
68 :queries => :index,
68 :queries => :index,
69 :reports => [:issue_report, :issue_report_details]}
69 :reports => [:issue_report, :issue_report_details]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
72 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
72 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
73 map.permission :manage_subtasks, {}
73 map.permission :manage_subtasks, {}
74 map.permission :set_issues_private, {}
74 map.permission :set_issues_private, {}
75 map.permission :set_own_issues_private, {}, :require => :loggedin
75 map.permission :set_own_issues_private, {}, :require => :loggedin
76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
79 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
79 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
80 map.permission :delete_issues, {:issues => :destroy}, :require => :member
80 map.permission :delete_issues, {:issues => :destroy}, :require => :member
81 # Queries
81 # Queries
82 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
82 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
83 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
83 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
84 # Watchers
84 # Watchers
85 map.permission :view_issue_watchers, {}
85 map.permission :view_issue_watchers, {}
86 map.permission :add_issue_watchers, {:watchers => :new}
86 map.permission :add_issue_watchers, {:watchers => :new}
87 map.permission :delete_issue_watchers, {:watchers => :destroy}
87 map.permission :delete_issue_watchers, {:watchers => :destroy}
88 end
88 end
89
89
90 map.project_module :time_tracking do |map|
90 map.project_module :time_tracking do |map|
91 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
91 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
92 map.permission :view_time_entries, :timelog => [:index, :report, :show]
92 map.permission :view_time_entries, :timelog => [:index, :report, :show]
93 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
93 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
94 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
94 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
95 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
95 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
96 end
96 end
97
97
98 map.project_module :news do |map|
98 map.project_module :news do |map|
99 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
99 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
100 map.permission :view_news, {:news => [:index, :show]}, :public => true
100 map.permission :view_news, {:news => [:index, :show]}, :public => true
101 map.permission :comment_news, {:comments => :create}
101 map.permission :comment_news, {:comments => :create}
102 end
102 end
103
103
104 map.project_module :documents do |map|
104 map.project_module :documents do |map|
105 map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
105 map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
106 map.permission :view_documents, :documents => [:index, :show, :download]
106 map.permission :view_documents, :documents => [:index, :show, :download]
107 end
107 end
108
108
109 map.project_module :files do |map|
109 map.project_module :files do |map|
110 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
110 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
111 map.permission :view_files, :files => :index, :versions => :download
111 map.permission :view_files, :files => :index, :versions => :download
112 end
112 end
113
113
114 map.project_module :wiki do |map|
114 map.project_module :wiki do |map|
115 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
115 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
116 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
116 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
117 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
117 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
118 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
118 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
119 map.permission :export_wiki_pages, :wiki => [:export]
119 map.permission :export_wiki_pages, :wiki => [:export]
120 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
120 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
121 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
121 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
122 map.permission :delete_wiki_pages_attachments, {}
122 map.permission :delete_wiki_pages_attachments, {}
123 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
123 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
124 end
124 end
125
125
126 map.project_module :repository do |map|
126 map.project_module :repository do |map|
127 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
127 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
128 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
128 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
129 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
129 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
130 map.permission :commit_access, {}
130 map.permission :commit_access, {}
131 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
131 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
132 end
132 end
133
133
134 map.project_module :boards do |map|
134 map.project_module :boards do |map|
135 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
135 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
136 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
136 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
137 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
137 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
138 map.permission :edit_messages, {:messages => :edit}, :require => :member
138 map.permission :edit_messages, {:messages => :edit}, :require => :member
139 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
139 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
140 map.permission :delete_messages, {:messages => :destroy}, :require => :member
140 map.permission :delete_messages, {:messages => :destroy}, :require => :member
141 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
141 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
142 end
142 end
143
143
144 map.project_module :calendar do |map|
144 map.project_module :calendar do |map|
145 map.permission :view_calendar, :calendars => [:show, :update]
145 map.permission :view_calendar, :calendars => [:show, :update]
146 end
146 end
147
147
148 map.project_module :gantt do |map|
148 map.project_module :gantt do |map|
149 map.permission :view_gantt, :gantts => [:show, :update]
149 map.permission :view_gantt, :gantts => [:show, :update]
150 end
150 end
151 end
151 end
152
152
153 Redmine::MenuManager.map :top_menu do |menu|
153 Redmine::MenuManager.map :top_menu do |menu|
154 menu.push :home, :home_path
154 menu.push :home, :home_path
155 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
155 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
156 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
156 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
157 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
157 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
158 menu.push :help, Redmine::Info.help_url, :last => true
158 menu.push :help, Redmine::Info.help_url, :last => true
159 end
159 end
160
160
161 Redmine::MenuManager.map :account_menu do |menu|
161 Redmine::MenuManager.map :account_menu do |menu|
162 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
162 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
163 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
163 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
164 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
164 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
165 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
165 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
166 end
166 end
167
167
168 Redmine::MenuManager.map :application_menu do |menu|
168 Redmine::MenuManager.map :application_menu do |menu|
169 # Empty
169 # Empty
170 end
170 end
171
171
172 Redmine::MenuManager.map :admin_menu do |menu|
172 Redmine::MenuManager.map :admin_menu do |menu|
173 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
173 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
174 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
174 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
175 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
175 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
176 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
176 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
177 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
177 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
178 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
178 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
179 :html => {:class => 'issue_statuses'}
179 :html => {:class => 'issue_statuses'}
180 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
180 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
181 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
181 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
182 :html => {:class => 'custom_fields'}
182 :html => {:class => 'custom_fields'}
183 menu.push :enumerations, {:controller => 'enumerations'}
183 menu.push :enumerations, {:controller => 'enumerations'}
184 menu.push :settings, {:controller => 'settings'}
184 menu.push :settings, {:controller => 'settings'}
185 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
185 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
186 :html => {:class => 'server_authentication'}
186 :html => {:class => 'server_authentication'}
187 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
187 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
188 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
188 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
189 end
189 end
190
190
191 Redmine::MenuManager.map :project_menu do |menu|
191 Redmine::MenuManager.map :project_menu do |menu|
192 menu.push :overview, { :controller => 'projects', :action => 'show' }
192 menu.push :overview, { :controller => 'projects', :action => 'show' }
193 menu.push :activity, { :controller => 'activities', :action => 'index' }
193 menu.push :activity, { :controller => 'activities', :action => 'index' }
194 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
194 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
195 :if => Proc.new { |p| p.shared_versions.any? }
195 :if => Proc.new { |p| p.shared_versions.any? }
196 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
196 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
197 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
197 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
198 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
198 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
199 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
199 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
200 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
200 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
201 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
201 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
202 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
202 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
203 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
203 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
204 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
204 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
205 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
205 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
206 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
206 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
207 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
207 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
208 menu.push :repository, { :controller => 'repositories', :action => 'show' },
208 menu.push :repository, { :controller => 'repositories', :action => 'show' },
209 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
209 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
210 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
210 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
211 end
211 end
212
212
213 Redmine::Activity.map do |activity|
213 Redmine::Activity.map do |activity|
214 activity.register :issues, :class_name => %w(Issue Journal)
214 activity.register :issues, :class_name => %w(Issue Journal)
215 activity.register :changesets
215 activity.register :changesets
216 activity.register :news
216 activity.register :news
217 activity.register :documents, :class_name => %w(Document Attachment)
217 activity.register :documents, :class_name => %w(Document Attachment)
218 activity.register :files, :class_name => 'Attachment'
218 activity.register :files, :class_name => 'Attachment'
219 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
219 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
220 activity.register :messages, :default => false
220 activity.register :messages, :default => false
221 activity.register :time_entries, :default => false
221 activity.register :time_entries, :default => false
222 end
222 end
223
223
224 Redmine::Search.map do |search|
224 Redmine::Search.map do |search|
225 search.register :issues
225 search.register :issues
226 search.register :news
226 search.register :news
227 search.register :documents
227 search.register :documents
228 search.register :changesets
228 search.register :changesets
229 search.register :wiki_pages
229 search.register :wiki_pages
230 search.register :messages
230 search.register :messages
231 search.register :projects
231 search.register :projects
232 end
232 end
233
233
234 Redmine::WikiFormatting.map do |format|
234 Redmine::WikiFormatting.map do |format|
235 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
235 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
236 end
236 end
237
237
238 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
238 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,85 +1,120
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::AttachmentsTest < ActionController::IntegrationTest
20 class ApiTest::AttachmentsTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :attachments
29 :attachments
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 set_fixtures_attachments_directory
33 set_fixtures_attachments_directory
34 end
34 end
35
35
36 def teardown
36 def teardown
37 set_tmp_attachments_directory
37 set_tmp_attachments_directory
38 end
38 end
39
39
40 context "/attachments/:id" do
40 context "/attachments/:id" do
41 context "GET" do
41 context "GET" do
42 should "return the attachment" do
42 should "return the attachment" do
43 get '/attachments/7.xml', {}, credentials('jsmith')
43 get '/attachments/7.xml', {}, credentials('jsmith')
44 assert_response :success
44 assert_response :success
45 assert_equal 'application/xml', @response.content_type
45 assert_equal 'application/xml', @response.content_type
46 assert_tag :tag => 'attachment',
46 assert_tag :tag => 'attachment',
47 :child => {
47 :child => {
48 :tag => 'id',
48 :tag => 'id',
49 :content => '7',
49 :content => '7',
50 :sibling => {
50 :sibling => {
51 :tag => 'filename',
51 :tag => 'filename',
52 :content => 'archive.zip',
52 :content => 'archive.zip',
53 :sibling => {
53 :sibling => {
54 :tag => 'content_url',
54 :tag => 'content_url',
55 :content => 'http://www.example.com/attachments/download/7/archive.zip'
55 :content => 'http://www.example.com/attachments/download/7/archive.zip'
56 }
56 }
57 }
57 }
58 }
58 }
59 end
59 end
60
60
61 should "deny access without credentials" do
61 should "deny access without credentials" do
62 get '/attachments/7.xml'
62 get '/attachments/7.xml'
63 assert_response 401
63 assert_response 401
64 set_tmp_attachments_directory
64 set_tmp_attachments_directory
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 context "/attachments/download/:id/:filename" do
69 context "/attachments/download/:id/:filename" do
70 context "GET" do
70 context "GET" do
71 should "return the attachment content" do
71 should "return the attachment content" do
72 get '/attachments/download/7/archive.zip', {}, credentials('jsmith')
72 get '/attachments/download/7/archive.zip', {}, credentials('jsmith')
73 assert_response :success
73 assert_response :success
74 assert_equal 'application/octet-stream', @response.content_type
74 assert_equal 'application/octet-stream', @response.content_type
75 set_tmp_attachments_directory
75 set_tmp_attachments_directory
76 end
76 end
77
77
78 should "deny access without credentials" do
78 should "deny access without credentials" do
79 get '/attachments/download/7/archive.zip'
79 get '/attachments/download/7/archive.zip'
80 assert_response 302
80 assert_response 302
81 set_tmp_attachments_directory
81 set_tmp_attachments_directory
82 end
82 end
83 end
83 end
84 end
84 end
85
86 context "POST /uploads" do
87 should "return the token" do
88 set_tmp_attachments_directory
89 assert_difference 'Attachment.count' do
90 post '/uploads.xml', 'File content', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
91 assert_response :created
92 assert_equal 'application/xml', response.content_type
93
94 xml = Hash.from_xml(response.body)
95 assert_kind_of Hash, xml['upload']
96 token = xml['upload']['token']
97 assert_not_nil token
98
99 attachment = Attachment.first(:order => 'id DESC')
100 assert_equal token, attachment.token
101 assert_nil attachment.container
102 assert_equal 2, attachment.author_id
103 assert_equal 'File content'.size, attachment.filesize
104 assert attachment.content_type.blank?
105 assert attachment.filename.present?
106 assert_match /\d+_[0-9a-z]+/, attachment.diskfile
107 assert File.exist?(attachment.diskfile)
108 assert_equal 'File content', File.read(attachment.diskfile)
109 end
110 end
111
112 should "not accept other content types" do
113 set_tmp_attachments_directory
114 assert_no_difference 'Attachment.count' do
115 post '/uploads.xml', 'PNG DATA', {'Content-Type' => 'image/png'}.merge(credentials('jsmith'))
116 assert_response 406
117 end
118 end
119 end
85 end
120 end
@@ -1,710 +1,778
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :versions,
28 :versions,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :issue_categories,
31 :issue_categories,
32 :enabled_modules,
32 :enabled_modules,
33 :enumerations,
33 :enumerations,
34 :attachments,
34 :attachments,
35 :workflows,
35 :workflows,
36 :custom_fields,
36 :custom_fields,
37 :custom_values,
37 :custom_values,
38 :custom_fields_projects,
38 :custom_fields_projects,
39 :custom_fields_trackers,
39 :custom_fields_trackers,
40 :time_entries,
40 :time_entries,
41 :journals,
41 :journals,
42 :journal_details,
42 :journal_details,
43 :queries,
43 :queries,
44 :attachments
44 :attachments
45
45
46 def setup
46 def setup
47 Setting.rest_api_enabled = '1'
47 Setting.rest_api_enabled = '1'
48 end
48 end
49
49
50 context "/issues" do
50 context "/issues" do
51 # Use a private project to make sure auth is really working and not just
51 # Use a private project to make sure auth is really working and not just
52 # only showing public issues.
52 # only showing public issues.
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54
54
55 should "contain metadata" do
55 should "contain metadata" do
56 get '/issues.xml'
56 get '/issues.xml'
57
57
58 assert_tag :tag => 'issues',
58 assert_tag :tag => 'issues',
59 :attributes => {
59 :attributes => {
60 :type => 'array',
60 :type => 'array',
61 :total_count => assigns(:issue_count),
61 :total_count => assigns(:issue_count),
62 :limit => 25,
62 :limit => 25,
63 :offset => 0
63 :offset => 0
64 }
64 }
65 end
65 end
66
66
67 context "with offset and limit" do
67 context "with offset and limit" do
68 should "use the params" do
68 should "use the params" do
69 get '/issues.xml?offset=2&limit=3'
69 get '/issues.xml?offset=2&limit=3'
70
70
71 assert_equal 3, assigns(:limit)
71 assert_equal 3, assigns(:limit)
72 assert_equal 2, assigns(:offset)
72 assert_equal 2, assigns(:offset)
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 end
74 end
75 end
75 end
76
76
77 context "with nometa param" do
77 context "with nometa param" do
78 should "not contain metadata" do
78 should "not contain metadata" do
79 get '/issues.xml?nometa=1'
79 get '/issues.xml?nometa=1'
80
80
81 assert_tag :tag => 'issues',
81 assert_tag :tag => 'issues',
82 :attributes => {
82 :attributes => {
83 :type => 'array',
83 :type => 'array',
84 :total_count => nil,
84 :total_count => nil,
85 :limit => nil,
85 :limit => nil,
86 :offset => nil
86 :offset => nil
87 }
87 }
88 end
88 end
89 end
89 end
90
90
91 context "with nometa header" do
91 context "with nometa header" do
92 should "not contain metadata" do
92 should "not contain metadata" do
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94
94
95 assert_tag :tag => 'issues',
95 assert_tag :tag => 'issues',
96 :attributes => {
96 :attributes => {
97 :type => 'array',
97 :type => 'array',
98 :total_count => nil,
98 :total_count => nil,
99 :limit => nil,
99 :limit => nil,
100 :offset => nil
100 :offset => nil
101 }
101 }
102 end
102 end
103 end
103 end
104
104
105 context "with relations" do
105 context "with relations" do
106 should "display relations" do
106 should "display relations" do
107 get '/issues.xml?include=relations'
107 get '/issues.xml?include=relations'
108
108
109 assert_response :success
109 assert_response :success
110 assert_equal 'application/xml', @response.content_type
110 assert_equal 'application/xml', @response.content_type
111 assert_tag 'relations',
111 assert_tag 'relations',
112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
113 :children => {:count => 1},
113 :children => {:count => 1},
114 :child => {
114 :child => {
115 :tag => 'relation',
115 :tag => 'relation',
116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
117 }
117 }
118 assert_tag 'relations',
118 assert_tag 'relations',
119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
120 :children => {:count => 0}
120 :children => {:count => 0}
121 end
121 end
122 end
122 end
123
123
124 context "with invalid query params" do
124 context "with invalid query params" do
125 should "return errors" do
125 should "return errors" do
126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
127
127
128 assert_response :unprocessable_entity
128 assert_response :unprocessable_entity
129 assert_equal 'application/xml', @response.content_type
129 assert_equal 'application/xml', @response.content_type
130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
131 end
131 end
132 end
132 end
133
133
134 context "with custom field filter" do
134 context "with custom field filter" do
135 should "show only issues with the custom field value" do
135 should "show only issues with the custom field value" do
136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
137
137
138 expected_ids = Issue.visible.all(
138 expected_ids = Issue.visible.all(
139 :include => :custom_values,
139 :include => :custom_values,
140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
141
141
142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
144 end
144 end
145 end
145 end
146 end
146 end
147
147
148 context "with custom field filter (shorthand method)" do
148 context "with custom field filter (shorthand method)" do
149 should "show only issues with the custom field value" do
149 should "show only issues with the custom field value" do
150 get '/issues.xml', { :cf_1 => 'MySQL' }
150 get '/issues.xml', { :cf_1 => 'MySQL' }
151
151
152 expected_ids = Issue.visible.all(
152 expected_ids = Issue.visible.all(
153 :include => :custom_values,
153 :include => :custom_values,
154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
155
155
156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
158 end
158 end
159 end
159 end
160 end
160 end
161 end
161 end
162
162
163 context "/index.json" do
163 context "/index.json" do
164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
165 end
165 end
166
166
167 context "/index.xml with filter" do
167 context "/index.xml with filter" do
168 should "show only issues with the status_id" do
168 should "show only issues with the status_id" do
169 get '/issues.xml?status_id=5'
169 get '/issues.xml?status_id=5'
170
170
171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
172
172
173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
175 end
175 end
176 end
176 end
177 end
177 end
178
178
179 context "/index.json with filter" do
179 context "/index.json with filter" do
180 should "show only issues with the status_id" do
180 should "show only issues with the status_id" do
181 get '/issues.json?status_id=5'
181 get '/issues.json?status_id=5'
182
182
183 json = ActiveSupport::JSON.decode(response.body)
183 json = ActiveSupport::JSON.decode(response.body)
184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
185 assert_equal 3, status_ids_used.length
185 assert_equal 3, status_ids_used.length
186 assert status_ids_used.all? {|id| id == 5 }
186 assert status_ids_used.all? {|id| id == 5 }
187 end
187 end
188
188
189 end
189 end
190
190
191 # Issue 6 is on a private project
191 # Issue 6 is on a private project
192 context "/issues/6.xml" do
192 context "/issues/6.xml" do
193 should_allow_api_authentication(:get, "/issues/6.xml")
193 should_allow_api_authentication(:get, "/issues/6.xml")
194 end
194 end
195
195
196 context "/issues/6.json" do
196 context "/issues/6.json" do
197 should_allow_api_authentication(:get, "/issues/6.json")
197 should_allow_api_authentication(:get, "/issues/6.json")
198 end
198 end
199
199
200 context "GET /issues/:id" do
200 context "GET /issues/:id" do
201 context "with journals" do
201 context "with journals" do
202 context ".xml" do
202 context ".xml" do
203 should "display journals" do
203 should "display journals" do
204 get '/issues/1.xml?include=journals'
204 get '/issues/1.xml?include=journals'
205
205
206 assert_tag :tag => 'issue',
206 assert_tag :tag => 'issue',
207 :child => {
207 :child => {
208 :tag => 'journals',
208 :tag => 'journals',
209 :attributes => { :type => 'array' },
209 :attributes => { :type => 'array' },
210 :child => {
210 :child => {
211 :tag => 'journal',
211 :tag => 'journal',
212 :attributes => { :id => '1'},
212 :attributes => { :id => '1'},
213 :child => {
213 :child => {
214 :tag => 'details',
214 :tag => 'details',
215 :attributes => { :type => 'array' },
215 :attributes => { :type => 'array' },
216 :child => {
216 :child => {
217 :tag => 'detail',
217 :tag => 'detail',
218 :attributes => { :name => 'status_id' },
218 :attributes => { :name => 'status_id' },
219 :child => {
219 :child => {
220 :tag => 'old_value',
220 :tag => 'old_value',
221 :content => '1',
221 :content => '1',
222 :sibling => {
222 :sibling => {
223 :tag => 'new_value',
223 :tag => 'new_value',
224 :content => '2'
224 :content => '2'
225 }
225 }
226 }
226 }
227 }
227 }
228 }
228 }
229 }
229 }
230 }
230 }
231 end
231 end
232 end
232 end
233 end
233 end
234
234
235 context "with custom fields" do
235 context "with custom fields" do
236 context ".xml" do
236 context ".xml" do
237 should "display custom fields" do
237 should "display custom fields" do
238 get '/issues/3.xml'
238 get '/issues/3.xml'
239
239
240 assert_tag :tag => 'issue',
240 assert_tag :tag => 'issue',
241 :child => {
241 :child => {
242 :tag => 'custom_fields',
242 :tag => 'custom_fields',
243 :attributes => { :type => 'array' },
243 :attributes => { :type => 'array' },
244 :child => {
244 :child => {
245 :tag => 'custom_field',
245 :tag => 'custom_field',
246 :attributes => { :id => '1'},
246 :attributes => { :id => '1'},
247 :child => {
247 :child => {
248 :tag => 'value',
248 :tag => 'value',
249 :content => 'MySQL'
249 :content => 'MySQL'
250 }
250 }
251 }
251 }
252 }
252 }
253
253
254 assert_nothing_raised do
254 assert_nothing_raised do
255 Hash.from_xml(response.body).to_xml
255 Hash.from_xml(response.body).to_xml
256 end
256 end
257 end
257 end
258 end
258 end
259 end
259 end
260
260
261 context "with multi custom fields" do
261 context "with multi custom fields" do
262 setup do
262 setup do
263 field = CustomField.find(1)
263 field = CustomField.find(1)
264 field.update_attribute :multiple, true
264 field.update_attribute :multiple, true
265 issue = Issue.find(3)
265 issue = Issue.find(3)
266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
267 issue.save!
267 issue.save!
268 end
268 end
269
269
270 context ".xml" do
270 context ".xml" do
271 should "display custom fields" do
271 should "display custom fields" do
272 get '/issues/3.xml'
272 get '/issues/3.xml'
273 assert_response :success
273 assert_response :success
274 assert_tag :tag => 'issue',
274 assert_tag :tag => 'issue',
275 :child => {
275 :child => {
276 :tag => 'custom_fields',
276 :tag => 'custom_fields',
277 :attributes => { :type => 'array' },
277 :attributes => { :type => 'array' },
278 :child => {
278 :child => {
279 :tag => 'custom_field',
279 :tag => 'custom_field',
280 :attributes => { :id => '1'},
280 :attributes => { :id => '1'},
281 :child => {
281 :child => {
282 :tag => 'value',
282 :tag => 'value',
283 :attributes => { :type => 'array' },
283 :attributes => { :type => 'array' },
284 :children => { :count => 2 }
284 :children => { :count => 2 }
285 }
285 }
286 }
286 }
287 }
287 }
288
288
289 xml = Hash.from_xml(response.body)
289 xml = Hash.from_xml(response.body)
290 custom_fields = xml['issue']['custom_fields']
290 custom_fields = xml['issue']['custom_fields']
291 assert_kind_of Array, custom_fields
291 assert_kind_of Array, custom_fields
292 field = custom_fields.detect {|f| f['id'] == '1'}
292 field = custom_fields.detect {|f| f['id'] == '1'}
293 assert_kind_of Hash, field
293 assert_kind_of Hash, field
294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
295 end
295 end
296 end
296 end
297
297
298 context ".json" do
298 context ".json" do
299 should "display custom fields" do
299 should "display custom fields" do
300 get '/issues/3.json'
300 get '/issues/3.json'
301 assert_response :success
301 assert_response :success
302 json = ActiveSupport::JSON.decode(response.body)
302 json = ActiveSupport::JSON.decode(response.body)
303 custom_fields = json['issue']['custom_fields']
303 custom_fields = json['issue']['custom_fields']
304 assert_kind_of Array, custom_fields
304 assert_kind_of Array, custom_fields
305 field = custom_fields.detect {|f| f['id'] == 1}
305 field = custom_fields.detect {|f| f['id'] == 1}
306 assert_kind_of Hash, field
306 assert_kind_of Hash, field
307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
308 end
308 end
309 end
309 end
310 end
310 end
311
311
312 context "with empty value for multi custom field" do
312 context "with empty value for multi custom field" do
313 setup do
313 setup do
314 field = CustomField.find(1)
314 field = CustomField.find(1)
315 field.update_attribute :multiple, true
315 field.update_attribute :multiple, true
316 issue = Issue.find(3)
316 issue = Issue.find(3)
317 issue.custom_field_values = {1 => ['']}
317 issue.custom_field_values = {1 => ['']}
318 issue.save!
318 issue.save!
319 end
319 end
320
320
321 context ".xml" do
321 context ".xml" do
322 should "display custom fields" do
322 should "display custom fields" do
323 get '/issues/3.xml'
323 get '/issues/3.xml'
324 assert_response :success
324 assert_response :success
325 assert_tag :tag => 'issue',
325 assert_tag :tag => 'issue',
326 :child => {
326 :child => {
327 :tag => 'custom_fields',
327 :tag => 'custom_fields',
328 :attributes => { :type => 'array' },
328 :attributes => { :type => 'array' },
329 :child => {
329 :child => {
330 :tag => 'custom_field',
330 :tag => 'custom_field',
331 :attributes => { :id => '1'},
331 :attributes => { :id => '1'},
332 :child => {
332 :child => {
333 :tag => 'value',
333 :tag => 'value',
334 :attributes => { :type => 'array' },
334 :attributes => { :type => 'array' },
335 :children => { :count => 0 }
335 :children => { :count => 0 }
336 }
336 }
337 }
337 }
338 }
338 }
339
339
340 xml = Hash.from_xml(response.body)
340 xml = Hash.from_xml(response.body)
341 custom_fields = xml['issue']['custom_fields']
341 custom_fields = xml['issue']['custom_fields']
342 assert_kind_of Array, custom_fields
342 assert_kind_of Array, custom_fields
343 field = custom_fields.detect {|f| f['id'] == '1'}
343 field = custom_fields.detect {|f| f['id'] == '1'}
344 assert_kind_of Hash, field
344 assert_kind_of Hash, field
345 assert_equal [], field['value']
345 assert_equal [], field['value']
346 end
346 end
347 end
347 end
348
348
349 context ".json" do
349 context ".json" do
350 should "display custom fields" do
350 should "display custom fields" do
351 get '/issues/3.json'
351 get '/issues/3.json'
352 assert_response :success
352 assert_response :success
353 json = ActiveSupport::JSON.decode(response.body)
353 json = ActiveSupport::JSON.decode(response.body)
354 custom_fields = json['issue']['custom_fields']
354 custom_fields = json['issue']['custom_fields']
355 assert_kind_of Array, custom_fields
355 assert_kind_of Array, custom_fields
356 field = custom_fields.detect {|f| f['id'] == 1}
356 field = custom_fields.detect {|f| f['id'] == 1}
357 assert_kind_of Hash, field
357 assert_kind_of Hash, field
358 assert_equal [], field['value'].sort
358 assert_equal [], field['value'].sort
359 end
359 end
360 end
360 end
361 end
361 end
362
362
363 context "with attachments" do
363 context "with attachments" do
364 context ".xml" do
364 context ".xml" do
365 should "display attachments" do
365 should "display attachments" do
366 get '/issues/3.xml?include=attachments'
366 get '/issues/3.xml?include=attachments'
367
367
368 assert_tag :tag => 'issue',
368 assert_tag :tag => 'issue',
369 :child => {
369 :child => {
370 :tag => 'attachments',
370 :tag => 'attachments',
371 :children => {:count => 5},
371 :children => {:count => 5},
372 :child => {
372 :child => {
373 :tag => 'attachment',
373 :tag => 'attachment',
374 :child => {
374 :child => {
375 :tag => 'filename',
375 :tag => 'filename',
376 :content => 'source.rb',
376 :content => 'source.rb',
377 :sibling => {
377 :sibling => {
378 :tag => 'content_url',
378 :tag => 'content_url',
379 :content => 'http://www.example.com/attachments/download/4/source.rb'
379 :content => 'http://www.example.com/attachments/download/4/source.rb'
380 }
380 }
381 }
381 }
382 }
382 }
383 }
383 }
384 end
384 end
385 end
385 end
386 end
386 end
387
387
388 context "with subtasks" do
388 context "with subtasks" do
389 setup do
389 setup do
390 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
390 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
391 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
391 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
392 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
392 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
393 end
393 end
394
394
395 context ".xml" do
395 context ".xml" do
396 should "display children" do
396 should "display children" do
397 get '/issues/1.xml?include=children'
397 get '/issues/1.xml?include=children'
398
398
399 assert_tag :tag => 'issue',
399 assert_tag :tag => 'issue',
400 :child => {
400 :child => {
401 :tag => 'children',
401 :tag => 'children',
402 :children => {:count => 2},
402 :children => {:count => 2},
403 :child => {
403 :child => {
404 :tag => 'issue',
404 :tag => 'issue',
405 :attributes => {:id => @c1.id.to_s},
405 :attributes => {:id => @c1.id.to_s},
406 :child => {
406 :child => {
407 :tag => 'subject',
407 :tag => 'subject',
408 :content => 'child c1',
408 :content => 'child c1',
409 :sibling => {
409 :sibling => {
410 :tag => 'children',
410 :tag => 'children',
411 :children => {:count => 1},
411 :children => {:count => 1},
412 :child => {
412 :child => {
413 :tag => 'issue',
413 :tag => 'issue',
414 :attributes => {:id => @c3.id.to_s}
414 :attributes => {:id => @c3.id.to_s}
415 }
415 }
416 }
416 }
417 }
417 }
418 }
418 }
419 }
419 }
420 end
420 end
421
421
422 context ".json" do
422 context ".json" do
423 should "display children" do
423 should "display children" do
424 get '/issues/1.json?include=children'
424 get '/issues/1.json?include=children'
425
425
426 json = ActiveSupport::JSON.decode(response.body)
426 json = ActiveSupport::JSON.decode(response.body)
427 assert_equal([
427 assert_equal([
428 {
428 {
429 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
429 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
430 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
430 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
431 },
431 },
432 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
432 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
433 ],
433 ],
434 json['issue']['children'])
434 json['issue']['children'])
435 end
435 end
436 end
436 end
437 end
437 end
438 end
438 end
439 end
439 end
440
440
441 context "POST /issues.xml" do
441 context "POST /issues.xml" do
442 should_allow_api_authentication(:post,
442 should_allow_api_authentication(:post,
443 '/issues.xml',
443 '/issues.xml',
444 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
444 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
445 {:success_code => :created})
445 {:success_code => :created})
446
446
447 should "create an issue with the attributes" do
447 should "create an issue with the attributes" do
448 assert_difference('Issue.count') do
448 assert_difference('Issue.count') do
449 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
449 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
450 end
450 end
451
451
452 issue = Issue.first(:order => 'id DESC')
452 issue = Issue.first(:order => 'id DESC')
453 assert_equal 1, issue.project_id
453 assert_equal 1, issue.project_id
454 assert_equal 2, issue.tracker_id
454 assert_equal 2, issue.tracker_id
455 assert_equal 3, issue.status_id
455 assert_equal 3, issue.status_id
456 assert_equal 'API test', issue.subject
456 assert_equal 'API test', issue.subject
457
457
458 assert_response :created
458 assert_response :created
459 assert_equal 'application/xml', @response.content_type
459 assert_equal 'application/xml', @response.content_type
460 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
460 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
461 end
461 end
462 end
462 end
463
463
464 context "POST /issues.xml with failure" do
464 context "POST /issues.xml with failure" do
465 should "have an errors tag" do
465 should "have an errors tag" do
466 assert_no_difference('Issue.count') do
466 assert_no_difference('Issue.count') do
467 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
467 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
468 end
468 end
469
469
470 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
470 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
471 end
471 end
472 end
472 end
473
473
474 context "POST /issues.json" do
474 context "POST /issues.json" do
475 should_allow_api_authentication(:post,
475 should_allow_api_authentication(:post,
476 '/issues.json',
476 '/issues.json',
477 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
477 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
478 {:success_code => :created})
478 {:success_code => :created})
479
479
480 should "create an issue with the attributes" do
480 should "create an issue with the attributes" do
481 assert_difference('Issue.count') do
481 assert_difference('Issue.count') do
482 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
482 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
483 end
483 end
484
484
485 issue = Issue.first(:order => 'id DESC')
485 issue = Issue.first(:order => 'id DESC')
486 assert_equal 1, issue.project_id
486 assert_equal 1, issue.project_id
487 assert_equal 2, issue.tracker_id
487 assert_equal 2, issue.tracker_id
488 assert_equal 3, issue.status_id
488 assert_equal 3, issue.status_id
489 assert_equal 'API test', issue.subject
489 assert_equal 'API test', issue.subject
490 end
490 end
491
491
492 end
492 end
493
493
494 context "POST /issues.json with failure" do
494 context "POST /issues.json with failure" do
495 should "have an errors element" do
495 should "have an errors element" do
496 assert_no_difference('Issue.count') do
496 assert_no_difference('Issue.count') do
497 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
497 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
498 end
498 end
499
499
500 json = ActiveSupport::JSON.decode(response.body)
500 json = ActiveSupport::JSON.decode(response.body)
501 assert json['errors'].include?(['subject', "can't be blank"])
501 assert json['errors'].include?(['subject', "can't be blank"])
502 end
502 end
503 end
503 end
504
504
505 # Issue 6 is on a private project
505 # Issue 6 is on a private project
506 context "PUT /issues/6.xml" do
506 context "PUT /issues/6.xml" do
507 setup do
507 setup do
508 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
508 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
509 end
509 end
510
510
511 should_allow_api_authentication(:put,
511 should_allow_api_authentication(:put,
512 '/issues/6.xml',
512 '/issues/6.xml',
513 {:issue => {:subject => 'API update', :notes => 'A new note'}},
513 {:issue => {:subject => 'API update', :notes => 'A new note'}},
514 {:success_code => :ok})
514 {:success_code => :ok})
515
515
516 should "not create a new issue" do
516 should "not create a new issue" do
517 assert_no_difference('Issue.count') do
517 assert_no_difference('Issue.count') do
518 put '/issues/6.xml', @parameters, credentials('jsmith')
518 put '/issues/6.xml', @parameters, credentials('jsmith')
519 end
519 end
520 end
520 end
521
521
522 should "create a new journal" do
522 should "create a new journal" do
523 assert_difference('Journal.count') do
523 assert_difference('Journal.count') do
524 put '/issues/6.xml', @parameters, credentials('jsmith')
524 put '/issues/6.xml', @parameters, credentials('jsmith')
525 end
525 end
526 end
526 end
527
527
528 should "add the note to the journal" do
528 should "add the note to the journal" do
529 put '/issues/6.xml', @parameters, credentials('jsmith')
529 put '/issues/6.xml', @parameters, credentials('jsmith')
530
530
531 journal = Journal.last
531 journal = Journal.last
532 assert_equal "A new note", journal.notes
532 assert_equal "A new note", journal.notes
533 end
533 end
534
534
535 should "update the issue" do
535 should "update the issue" do
536 put '/issues/6.xml', @parameters, credentials('jsmith')
536 put '/issues/6.xml', @parameters, credentials('jsmith')
537
537
538 issue = Issue.find(6)
538 issue = Issue.find(6)
539 assert_equal "API update", issue.subject
539 assert_equal "API update", issue.subject
540 end
540 end
541
541
542 end
542 end
543
543
544 context "PUT /issues/3.xml with custom fields" do
544 context "PUT /issues/3.xml with custom fields" do
545 setup do
545 setup do
546 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
546 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
547 end
547 end
548
548
549 should "update custom fields" do
549 should "update custom fields" do
550 assert_no_difference('Issue.count') do
550 assert_no_difference('Issue.count') do
551 put '/issues/3.xml', @parameters, credentials('jsmith')
551 put '/issues/3.xml', @parameters, credentials('jsmith')
552 end
552 end
553
553
554 issue = Issue.find(3)
554 issue = Issue.find(3)
555 assert_equal '150', issue.custom_value_for(2).value
555 assert_equal '150', issue.custom_value_for(2).value
556 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
556 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
557 end
557 end
558 end
558 end
559
559
560 context "PUT /issues/3.xml with multi custom fields" do
560 context "PUT /issues/3.xml with multi custom fields" do
561 setup do
561 setup do
562 field = CustomField.find(1)
562 field = CustomField.find(1)
563 field.update_attribute :multiple, true
563 field.update_attribute :multiple, true
564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
565 end
565 end
566
566
567 should "update custom fields" do
567 should "update custom fields" do
568 assert_no_difference('Issue.count') do
568 assert_no_difference('Issue.count') do
569 put '/issues/3.xml', @parameters, credentials('jsmith')
569 put '/issues/3.xml', @parameters, credentials('jsmith')
570 end
570 end
571
571
572 issue = Issue.find(3)
572 issue = Issue.find(3)
573 assert_equal '150', issue.custom_value_for(2).value
573 assert_equal '150', issue.custom_value_for(2).value
574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
575 end
575 end
576 end
576 end
577
577
578 context "PUT /issues/3.xml with project change" do
578 context "PUT /issues/3.xml with project change" do
579 setup do
579 setup do
580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
581 end
581 end
582
582
583 should "update project" do
583 should "update project" do
584 assert_no_difference('Issue.count') do
584 assert_no_difference('Issue.count') do
585 put '/issues/3.xml', @parameters, credentials('jsmith')
585 put '/issues/3.xml', @parameters, credentials('jsmith')
586 end
586 end
587
587
588 issue = Issue.find(3)
588 issue = Issue.find(3)
589 assert_equal 2, issue.project_id
589 assert_equal 2, issue.project_id
590 assert_equal 'Project changed', issue.subject
590 assert_equal 'Project changed', issue.subject
591 end
591 end
592 end
592 end
593
593
594 context "PUT /issues/6.xml with failed update" do
594 context "PUT /issues/6.xml with failed update" do
595 setup do
595 setup do
596 @parameters = {:issue => {:subject => ''}}
596 @parameters = {:issue => {:subject => ''}}
597 end
597 end
598
598
599 should "not create a new issue" do
599 should "not create a new issue" do
600 assert_no_difference('Issue.count') do
600 assert_no_difference('Issue.count') do
601 put '/issues/6.xml', @parameters, credentials('jsmith')
601 put '/issues/6.xml', @parameters, credentials('jsmith')
602 end
602 end
603 end
603 end
604
604
605 should "not create a new journal" do
605 should "not create a new journal" do
606 assert_no_difference('Journal.count') do
606 assert_no_difference('Journal.count') do
607 put '/issues/6.xml', @parameters, credentials('jsmith')
607 put '/issues/6.xml', @parameters, credentials('jsmith')
608 end
608 end
609 end
609 end
610
610
611 should "have an errors tag" do
611 should "have an errors tag" do
612 put '/issues/6.xml', @parameters, credentials('jsmith')
612 put '/issues/6.xml', @parameters, credentials('jsmith')
613
613
614 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
614 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
615 end
615 end
616 end
616 end
617
617
618 context "PUT /issues/6.json" do
618 context "PUT /issues/6.json" do
619 setup do
619 setup do
620 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
620 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
621 end
621 end
622
622
623 should_allow_api_authentication(:put,
623 should_allow_api_authentication(:put,
624 '/issues/6.json',
624 '/issues/6.json',
625 {:issue => {:subject => 'API update', :notes => 'A new note'}},
625 {:issue => {:subject => 'API update', :notes => 'A new note'}},
626 {:success_code => :ok})
626 {:success_code => :ok})
627
627
628 should "not create a new issue" do
628 should "not create a new issue" do
629 assert_no_difference('Issue.count') do
629 assert_no_difference('Issue.count') do
630 put '/issues/6.json', @parameters, credentials('jsmith')
630 put '/issues/6.json', @parameters, credentials('jsmith')
631 end
631 end
632 end
632 end
633
633
634 should "create a new journal" do
634 should "create a new journal" do
635 assert_difference('Journal.count') do
635 assert_difference('Journal.count') do
636 put '/issues/6.json', @parameters, credentials('jsmith')
636 put '/issues/6.json', @parameters, credentials('jsmith')
637 end
637 end
638 end
638 end
639
639
640 should "add the note to the journal" do
640 should "add the note to the journal" do
641 put '/issues/6.json', @parameters, credentials('jsmith')
641 put '/issues/6.json', @parameters, credentials('jsmith')
642
642
643 journal = Journal.last
643 journal = Journal.last
644 assert_equal "A new note", journal.notes
644 assert_equal "A new note", journal.notes
645 end
645 end
646
646
647 should "update the issue" do
647 should "update the issue" do
648 put '/issues/6.json', @parameters, credentials('jsmith')
648 put '/issues/6.json', @parameters, credentials('jsmith')
649
649
650 issue = Issue.find(6)
650 issue = Issue.find(6)
651 assert_equal "API update", issue.subject
651 assert_equal "API update", issue.subject
652 end
652 end
653
653
654 end
654 end
655
655
656 context "PUT /issues/6.json with failed update" do
656 context "PUT /issues/6.json with failed update" do
657 setup do
657 setup do
658 @parameters = {:issue => {:subject => ''}}
658 @parameters = {:issue => {:subject => ''}}
659 end
659 end
660
660
661 should "not create a new issue" do
661 should "not create a new issue" do
662 assert_no_difference('Issue.count') do
662 assert_no_difference('Issue.count') do
663 put '/issues/6.json', @parameters, credentials('jsmith')
663 put '/issues/6.json', @parameters, credentials('jsmith')
664 end
664 end
665 end
665 end
666
666
667 should "not create a new journal" do
667 should "not create a new journal" do
668 assert_no_difference('Journal.count') do
668 assert_no_difference('Journal.count') do
669 put '/issues/6.json', @parameters, credentials('jsmith')
669 put '/issues/6.json', @parameters, credentials('jsmith')
670 end
670 end
671 end
671 end
672
672
673 should "have an errors attribute" do
673 should "have an errors attribute" do
674 put '/issues/6.json', @parameters, credentials('jsmith')
674 put '/issues/6.json', @parameters, credentials('jsmith')
675
675
676 json = ActiveSupport::JSON.decode(response.body)
676 json = ActiveSupport::JSON.decode(response.body)
677 assert json['errors'].include?(['subject', "can't be blank"])
677 assert json['errors'].include?(['subject', "can't be blank"])
678 end
678 end
679 end
679 end
680
680
681 context "DELETE /issues/1.xml" do
681 context "DELETE /issues/1.xml" do
682 should_allow_api_authentication(:delete,
682 should_allow_api_authentication(:delete,
683 '/issues/6.xml',
683 '/issues/6.xml',
684 {},
684 {},
685 {:success_code => :ok})
685 {:success_code => :ok})
686
686
687 should "delete the issue" do
687 should "delete the issue" do
688 assert_difference('Issue.count',-1) do
688 assert_difference('Issue.count',-1) do
689 delete '/issues/6.xml', {}, credentials('jsmith')
689 delete '/issues/6.xml', {}, credentials('jsmith')
690 end
690 end
691
691
692 assert_nil Issue.find_by_id(6)
692 assert_nil Issue.find_by_id(6)
693 end
693 end
694 end
694 end
695
695
696 context "DELETE /issues/1.json" do
696 context "DELETE /issues/1.json" do
697 should_allow_api_authentication(:delete,
697 should_allow_api_authentication(:delete,
698 '/issues/6.json',
698 '/issues/6.json',
699 {},
699 {},
700 {:success_code => :ok})
700 {:success_code => :ok})
701
701
702 should "delete the issue" do
702 should "delete the issue" do
703 assert_difference('Issue.count',-1) do
703 assert_difference('Issue.count',-1) do
704 delete '/issues/6.json', {}, credentials('jsmith')
704 delete '/issues/6.json', {}, credentials('jsmith')
705 end
705 end
706
706
707 assert_nil Issue.find_by_id(6)
707 assert_nil Issue.find_by_id(6)
708 end
708 end
709 end
709 end
710
711 def test_create_issue_with_uploaded_file
712 set_tmp_attachments_directory
713
714 # upload the file
715 assert_difference 'Attachment.count' do
716 post '/uploads.xml', 'test_create_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
717 assert_response :created
718 end
719 xml = Hash.from_xml(response.body)
720 token = xml['upload']['token']
721 attachment = Attachment.first(:order => 'id DESC')
722
723 # create the issue with the upload's token
724 assert_difference 'Issue.count' do
725 post '/issues.xml',
726 {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
727 credentials('jsmith')
728 assert_response :created
729 end
730 issue = Issue.first(:order => 'id DESC')
731 assert_equal 1, issue.attachments.count
732 assert_equal attachment, issue.attachments.first
733
734 attachment.reload
735 assert_equal 'test.txt', attachment.filename
736 assert_equal 'text/plain', attachment.content_type
737 assert_equal 'test_create_with_upload'.size, attachment.filesize
738 assert_equal 2, attachment.author_id
739
740 # get the issue with its attachments
741 get "/issues/#{issue.id}.xml", :include => 'attachments'
742 assert_response :success
743 xml = Hash.from_xml(response.body)
744 attachments = xml['issue']['attachments']
745 assert_kind_of Array, attachments
746 assert_equal 1, attachments.size
747 url = attachments.first['content_url']
748 assert_not_nil url
749
750 # download the attachment
751 get url
752 assert_response :success
753 end
754
755 def test_update_issue_with_uploaded_file
756 set_tmp_attachments_directory
757
758 # upload the file
759 assert_difference 'Attachment.count' do
760 post '/uploads.xml', 'test_upload_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
761 assert_response :created
762 end
763 xml = Hash.from_xml(response.body)
764 token = xml['upload']['token']
765 attachment = Attachment.first(:order => 'id DESC')
766
767 # update the issue with the upload's token
768 assert_difference 'Journal.count' do
769 put '/issues/1.xml',
770 {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
771 credentials('jsmith')
772 assert_response :ok
773 end
774
775 issue = Issue.find(1)
776 assert_include attachment, issue.attachments
777 end
710 end
778 end
@@ -1,53 +1,61
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class RoutingAttachmentsTest < ActionController::IntegrationTest
20 class RoutingAttachmentsTest < ActionController::IntegrationTest
21 def test_attachments
21 def test_attachments
22 assert_routing(
22 assert_routing(
23 { :method => 'get', :path => "/attachments/1" },
23 { :method => 'get', :path => "/attachments/1" },
24 { :controller => 'attachments', :action => 'show', :id => '1' }
24 { :controller => 'attachments', :action => 'show', :id => '1' }
25 )
25 )
26 assert_routing(
26 assert_routing(
27 { :method => 'get', :path => "/attachments/1.xml" },
27 { :method => 'get', :path => "/attachments/1.xml" },
28 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' }
28 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' }
29 )
29 )
30 assert_routing(
30 assert_routing(
31 { :method => 'get', :path => "/attachments/1.json" },
31 { :method => 'get', :path => "/attachments/1.json" },
32 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' }
32 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' }
33 )
33 )
34 assert_routing(
34 assert_routing(
35 { :method => 'get', :path => "/attachments/1/filename.ext" },
35 { :method => 'get', :path => "/attachments/1/filename.ext" },
36 { :controller => 'attachments', :action => 'show', :id => '1',
36 { :controller => 'attachments', :action => 'show', :id => '1',
37 :filename => 'filename.ext' }
37 :filename => 'filename.ext' }
38 )
38 )
39 assert_routing(
39 assert_routing(
40 { :method => 'get', :path => "/attachments/download/1" },
40 { :method => 'get', :path => "/attachments/download/1" },
41 { :controller => 'attachments', :action => 'download', :id => '1' }
41 { :controller => 'attachments', :action => 'download', :id => '1' }
42 )
42 )
43 assert_routing(
43 assert_routing(
44 { :method => 'get', :path => "/attachments/download/1/filename.ext" },
44 { :method => 'get', :path => "/attachments/download/1/filename.ext" },
45 { :controller => 'attachments', :action => 'download', :id => '1',
45 { :controller => 'attachments', :action => 'download', :id => '1',
46 :filename => 'filename.ext' }
46 :filename => 'filename.ext' }
47 )
47 )
48 assert_routing(
48 assert_routing(
49 { :method => 'delete', :path => "/attachments/1" },
49 { :method => 'delete', :path => "/attachments/1" },
50 { :controller => 'attachments', :action => 'destroy', :id => '1' }
50 { :controller => 'attachments', :action => 'destroy', :id => '1' }
51 )
51 )
52 assert_routing(
53 { :method => 'post', :path => '/uploads.xml' },
54 { :controller => 'attachments', :action => 'upload', :format => 'xml' }
55 )
56 assert_routing(
57 { :method => 'post', :path => '/uploads.json' },
58 { :controller => 'attachments', :action => 'upload', :format => 'json' }
59 )
52 end
60 end
53 end
61 end
@@ -1,97 +1,102
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Attachable
20 module Attachable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_attachable(options = {})
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
28 self.attachable_options = {}
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31
31
32 has_many :attachments, options.merge(:as => :container,
32 has_many :attachments, options.merge(:as => :container,
33 :order => "#{Attachment.table_name}.created_on",
33 :order => "#{Attachment.table_name}.created_on",
34 :dependent => :destroy)
34 :dependent => :destroy)
35 send :include, Redmine::Acts::Attachable::InstanceMethods
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 before_save :attach_saved_attachments
36 before_save :attach_saved_attachments
37 end
37 end
38 end
38 end
39
39
40 module InstanceMethods
40 module InstanceMethods
41 def self.included(base)
41 def self.included(base)
42 base.extend ClassMethods
42 base.extend ClassMethods
43 end
43 end
44
44
45 def attachments_visible?(user=User.current)
45 def attachments_visible?(user=User.current)
46 (respond_to?(:visible?) ? visible?(user) : true) &&
46 (respond_to?(:visible?) ? visible?(user) : true) &&
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
48 end
48 end
49
49
50 def attachments_deletable?(user=User.current)
50 def attachments_deletable?(user=User.current)
51 (respond_to?(:visible?) ? visible?(user) : true) &&
51 (respond_to?(:visible?) ? visible?(user) : true) &&
52 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
52 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
53 end
53 end
54
54
55 def saved_attachments
55 def saved_attachments
56 @saved_attachments ||= []
56 @saved_attachments ||= []
57 end
57 end
58
58
59 def unsaved_attachments
59 def unsaved_attachments
60 @unsaved_attachments ||= []
60 @unsaved_attachments ||= []
61 end
61 end
62
62
63 def save_attachments(attachments, author=User.current)
63 def save_attachments(attachments, author=User.current)
64 if attachments && attachments.is_a?(Hash)
64 if attachments.is_a?(Hash)
65 attachments.each_value do |attachment|
65 attachments = attachments.values
66 end
67 if attachments.is_a?(Array)
68 attachments.each do |attachment|
66 a = nil
69 a = nil
67 if file = attachment['file']
70 if file = attachment['file']
68 next unless file && file.size > 0
71 next unless file.size > 0
69 a = Attachment.create(:file => file,
72 a = Attachment.create(:file => file, :author => author)
70 :description => attachment['description'].to_s.strip,
71 :author => author)
72 elsif token = attachment['token']
73 elsif token = attachment['token']
73 a = Attachment.find_by_token(token)
74 a = Attachment.find_by_token(token)
75 next unless a
76 a.filename = attachment['filename'] unless attachment['filename'].blank?
77 a.content_type = attachment['content_type']
74 end
78 end
75 next unless a
79 next unless a
80 a.description = attachment['description'].to_s.strip
76 if a.new_record?
81 if a.new_record?
77 unsaved_attachments << a
82 unsaved_attachments << a
78 else
83 else
79 saved_attachments << a
84 saved_attachments << a
80 end
85 end
81 end
86 end
82 end
87 end
83 {:files => saved_attachments, :unsaved => unsaved_attachments}
88 {:files => saved_attachments, :unsaved => unsaved_attachments}
84 end
89 end
85
90
86 def attach_saved_attachments
91 def attach_saved_attachments
87 saved_attachments.each do |attachment|
92 saved_attachments.each do |attachment|
88 self.attachments << attachment
93 self.attachments << attachment
89 end
94 end
90 end
95 end
91
96
92 module ClassMethods
97 module ClassMethods
93 end
98 end
94 end
99 end
95 end
100 end
96 end
101 end
97 end
102 end
General Comments 0
You need to be logged in to leave comments. Login now