##// END OF EJS Templates
Use head instead of render :nothing => true....
Jean-Philippe Lang -
r15305:cad0036297bd
parent child
Show More
@@ -1,202 +1,202
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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_action :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
19 before_action :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
20 before_action :find_editable_attachments, :only => [:edit, :update]
20 before_action :find_editable_attachments, :only => [:edit, :update]
21 before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
22 before_action :delete_authorize, :only => :destroy
22 before_action :delete_authorize, :only => :destroy
23 before_action :authorize_global, :only => :upload
23 before_action :authorize_global, :only => :upload
24
24
25 accept_api_auth :show, :download, :thumbnail, :upload, :destroy
25 accept_api_auth :show, :download, :thumbnail, :upload, :destroy
26
26
27 def show
27 def show
28 respond_to do |format|
28 respond_to do |format|
29 format.html {
29 format.html {
30 if @attachment.is_diff?
30 if @attachment.is_diff?
31 @diff = File.read(@attachment.diskfile, :mode => "rb")
31 @diff = File.read(@attachment.diskfile, :mode => "rb")
32 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
33 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
34 # Save diff type as user preference
34 # Save diff type as user preference
35 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
36 User.current.pref[:diff_type] = @diff_type
36 User.current.pref[:diff_type] = @diff_type
37 User.current.preference.save
37 User.current.preference.save
38 end
38 end
39 render :action => 'diff'
39 render :action => 'diff'
40 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
41 @content = File.read(@attachment.diskfile, :mode => "rb")
41 @content = File.read(@attachment.diskfile, :mode => "rb")
42 render :action => 'file'
42 render :action => 'file'
43 elsif @attachment.is_image?
43 elsif @attachment.is_image?
44 render :action => 'image'
44 render :action => 'image'
45 else
45 else
46 render :action => 'other'
46 render :action => 'other'
47 end
47 end
48 }
48 }
49 format.api
49 format.api
50 end
50 end
51 end
51 end
52
52
53 def download
53 def download
54 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
54 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
55 @attachment.increment_download
55 @attachment.increment_download
56 end
56 end
57
57
58 if stale?(:etag => @attachment.digest)
58 if stale?(:etag => @attachment.digest)
59 # images are sent inline
59 # images are sent inline
60 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
60 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
61 :type => detect_content_type(@attachment),
61 :type => detect_content_type(@attachment),
62 :disposition => disposition(@attachment)
62 :disposition => disposition(@attachment)
63 end
63 end
64 end
64 end
65
65
66 def thumbnail
66 def thumbnail
67 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
67 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
68 if stale?(:etag => tbnail)
68 if stale?(:etag => tbnail)
69 send_file tbnail,
69 send_file tbnail,
70 :filename => filename_for_content_disposition(@attachment.filename),
70 :filename => filename_for_content_disposition(@attachment.filename),
71 :type => detect_content_type(@attachment),
71 :type => detect_content_type(@attachment),
72 :disposition => 'inline'
72 :disposition => 'inline'
73 end
73 end
74 else
74 else
75 # No thumbnail for the attachment or thumbnail could not be created
75 # No thumbnail for the attachment or thumbnail could not be created
76 render :nothing => true, :status => 404
76 head 404
77 end
77 end
78 end
78 end
79
79
80 def upload
80 def upload
81 # Make sure that API users get used to set this content type
81 # Make sure that API users get used to set this content type
82 # as it won't trigger Rails' automatic parsing of the request body for parameters
82 # as it won't trigger Rails' automatic parsing of the request body for parameters
83 unless request.content_type == 'application/octet-stream'
83 unless request.content_type == 'application/octet-stream'
84 render :nothing => true, :status => 406
84 head 406
85 return
85 return
86 end
86 end
87
87
88 @attachment = Attachment.new(:file => request.raw_post)
88 @attachment = Attachment.new(:file => request.raw_post)
89 @attachment.author = User.current
89 @attachment.author = User.current
90 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
90 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
91 @attachment.content_type = params[:content_type].presence
91 @attachment.content_type = params[:content_type].presence
92 saved = @attachment.save
92 saved = @attachment.save
93
93
94 respond_to do |format|
94 respond_to do |format|
95 format.js
95 format.js
96 format.api {
96 format.api {
97 if saved
97 if saved
98 render :action => 'upload', :status => :created
98 render :action => 'upload', :status => :created
99 else
99 else
100 render_validation_errors(@attachment)
100 render_validation_errors(@attachment)
101 end
101 end
102 }
102 }
103 end
103 end
104 end
104 end
105
105
106 def edit
106 def edit
107 end
107 end
108
108
109 def update
109 def update
110 if params[:attachments].is_a?(Hash)
110 if params[:attachments].is_a?(Hash)
111 if Attachment.update_attachments(@attachments, params[:attachments])
111 if Attachment.update_attachments(@attachments, params[:attachments])
112 redirect_back_or_default home_path
112 redirect_back_or_default home_path
113 return
113 return
114 end
114 end
115 end
115 end
116 render :action => 'edit'
116 render :action => 'edit'
117 end
117 end
118
118
119 def destroy
119 def destroy
120 if @attachment.container.respond_to?(:init_journal)
120 if @attachment.container.respond_to?(:init_journal)
121 @attachment.container.init_journal(User.current)
121 @attachment.container.init_journal(User.current)
122 end
122 end
123 if @attachment.container
123 if @attachment.container
124 # Make sure association callbacks are called
124 # Make sure association callbacks are called
125 @attachment.container.attachments.delete(@attachment)
125 @attachment.container.attachments.delete(@attachment)
126 else
126 else
127 @attachment.destroy
127 @attachment.destroy
128 end
128 end
129
129
130 respond_to do |format|
130 respond_to do |format|
131 format.html { redirect_to_referer_or project_path(@project) }
131 format.html { redirect_to_referer_or project_path(@project) }
132 format.js
132 format.js
133 format.api { render_api_ok }
133 format.api { render_api_ok }
134 end
134 end
135 end
135 end
136
136
137 private
137 private
138
138
139 def find_attachment
139 def find_attachment
140 @attachment = Attachment.find(params[:id])
140 @attachment = Attachment.find(params[:id])
141 # Show 404 if the filename in the url is wrong
141 # Show 404 if the filename in the url is wrong
142 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
142 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
143 @project = @attachment.project
143 @project = @attachment.project
144 rescue ActiveRecord::RecordNotFound
144 rescue ActiveRecord::RecordNotFound
145 render_404
145 render_404
146 end
146 end
147
147
148 def find_editable_attachments
148 def find_editable_attachments
149 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
149 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
150 unless klass && klass.reflect_on_association(:attachments)
150 unless klass && klass.reflect_on_association(:attachments)
151 render_404
151 render_404
152 return
152 return
153 end
153 end
154
154
155 @container = klass.find(params[:object_id])
155 @container = klass.find(params[:object_id])
156 if @container.respond_to?(:visible?) && !@container.visible?
156 if @container.respond_to?(:visible?) && !@container.visible?
157 render_403
157 render_403
158 return
158 return
159 end
159 end
160 @attachments = @container.attachments.select(&:editable?)
160 @attachments = @container.attachments.select(&:editable?)
161 if @container.respond_to?(:project)
161 if @container.respond_to?(:project)
162 @project = @container.project
162 @project = @container.project
163 end
163 end
164 render_404 if @attachments.empty?
164 render_404 if @attachments.empty?
165 rescue ActiveRecord::RecordNotFound
165 rescue ActiveRecord::RecordNotFound
166 render_404
166 render_404
167 end
167 end
168
168
169 # Checks that the file exists and is readable
169 # Checks that the file exists and is readable
170 def file_readable
170 def file_readable
171 if @attachment.readable?
171 if @attachment.readable?
172 true
172 true
173 else
173 else
174 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
174 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
175 render_404
175 render_404
176 end
176 end
177 end
177 end
178
178
179 def read_authorize
179 def read_authorize
180 @attachment.visible? ? true : deny_access
180 @attachment.visible? ? true : deny_access
181 end
181 end
182
182
183 def delete_authorize
183 def delete_authorize
184 @attachment.deletable? ? true : deny_access
184 @attachment.deletable? ? true : deny_access
185 end
185 end
186
186
187 def detect_content_type(attachment)
187 def detect_content_type(attachment)
188 content_type = attachment.content_type
188 content_type = attachment.content_type
189 if content_type.blank? || content_type == "application/octet-stream"
189 if content_type.blank? || content_type == "application/octet-stream"
190 content_type = Redmine::MimeType.of(attachment.filename)
190 content_type = Redmine::MimeType.of(attachment.filename)
191 end
191 end
192 content_type.to_s
192 content_type.to_s
193 end
193 end
194
194
195 def disposition(attachment)
195 def disposition(attachment)
196 if attachment.is_image? || attachment.is_pdf?
196 if attachment.is_image? || attachment.is_pdf?
197 'inline'
197 'inline'
198 else
198 else
199 'attachment'
199 'attachment'
200 end
200 end
201 end
201 end
202 end
202 end
@@ -1,119 +1,119
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 BoardsController < ApplicationController
18 class BoardsController < ApplicationController
19 default_search_scope :messages
19 default_search_scope :messages
20 before_action :find_project_by_project_id, :find_board_if_available, :authorize
20 before_action :find_project_by_project_id, :find_board_if_available, :authorize
21 accept_rss_auth :index, :show
21 accept_rss_auth :index, :show
22
22
23 helper :sort
23 helper :sort
24 include SortHelper
24 include SortHelper
25 helper :watchers
25 helper :watchers
26
26
27 def index
27 def index
28 @boards = @project.boards.preload(:project, :last_message => :author).to_a
28 @boards = @project.boards.preload(:project, :last_message => :author).to_a
29 # show the board if there is only one
29 # show the board if there is only one
30 if @boards.size == 1
30 if @boards.size == 1
31 @board = @boards.first
31 @board = @boards.first
32 show
32 show
33 end
33 end
34 end
34 end
35
35
36 def show
36 def show
37 respond_to do |format|
37 respond_to do |format|
38 format.html {
38 format.html {
39 sort_init 'updated_on', 'desc'
39 sort_init 'updated_on', 'desc'
40 sort_update 'created_on' => "#{Message.table_name}.id",
40 sort_update 'created_on' => "#{Message.table_name}.id",
41 'replies' => "#{Message.table_name}.replies_count",
41 'replies' => "#{Message.table_name}.replies_count",
42 'updated_on' => "COALESCE(#{Message.table_name}.last_reply_id, #{Message.table_name}.id)"
42 'updated_on' => "COALESCE(#{Message.table_name}.last_reply_id, #{Message.table_name}.id)"
43
43
44 @topic_count = @board.topics.count
44 @topic_count = @board.topics.count
45 @topic_pages = Paginator.new @topic_count, per_page_option, params['page']
45 @topic_pages = Paginator.new @topic_count, per_page_option, params['page']
46 @topics = @board.topics.
46 @topics = @board.topics.
47 reorder(:sticky => :desc).
47 reorder(:sticky => :desc).
48 limit(@topic_pages.per_page).
48 limit(@topic_pages.per_page).
49 offset(@topic_pages.offset).
49 offset(@topic_pages.offset).
50 order(sort_clause).
50 order(sort_clause).
51 preload(:author, {:last_reply => :author}).
51 preload(:author, {:last_reply => :author}).
52 to_a
52 to_a
53 @message = Message.new(:board => @board)
53 @message = Message.new(:board => @board)
54 render :action => 'show', :layout => !request.xhr?
54 render :action => 'show', :layout => !request.xhr?
55 }
55 }
56 format.atom {
56 format.atom {
57 @messages = @board.messages.
57 @messages = @board.messages.
58 reorder(:id => :desc).
58 reorder(:id => :desc).
59 includes(:author, :board).
59 includes(:author, :board).
60 limit(Setting.feeds_limit.to_i).
60 limit(Setting.feeds_limit.to_i).
61 to_a
61 to_a
62 render_feed(@messages, :title => "#{@project}: #{@board}")
62 render_feed(@messages, :title => "#{@project}: #{@board}")
63 }
63 }
64 end
64 end
65 end
65 end
66
66
67 def new
67 def new
68 @board = @project.boards.build
68 @board = @project.boards.build
69 @board.safe_attributes = params[:board]
69 @board.safe_attributes = params[:board]
70 end
70 end
71
71
72 def create
72 def create
73 @board = @project.boards.build
73 @board = @project.boards.build
74 @board.safe_attributes = params[:board]
74 @board.safe_attributes = params[:board]
75 if @board.save
75 if @board.save
76 flash[:notice] = l(:notice_successful_create)
76 flash[:notice] = l(:notice_successful_create)
77 redirect_to_settings_in_projects
77 redirect_to_settings_in_projects
78 else
78 else
79 render :action => 'new'
79 render :action => 'new'
80 end
80 end
81 end
81 end
82
82
83 def edit
83 def edit
84 end
84 end
85
85
86 def update
86 def update
87 @board.safe_attributes = params[:board]
87 @board.safe_attributes = params[:board]
88 if @board.save
88 if @board.save
89 respond_to do |format|
89 respond_to do |format|
90 format.html {
90 format.html {
91 flash[:notice] = l(:notice_successful_update)
91 flash[:notice] = l(:notice_successful_update)
92 redirect_to_settings_in_projects
92 redirect_to_settings_in_projects
93 }
93 }
94 format.js { render :nothing => true }
94 format.js { head 200 }
95 end
95 end
96 else
96 else
97 respond_to do |format|
97 respond_to do |format|
98 format.html { render :action => 'edit' }
98 format.html { render :action => 'edit' }
99 format.js { render :nothing => true, :status => 422 }
99 format.js { head 422 }
100 end
100 end
101 end
101 end
102 end
102 end
103
103
104 def destroy
104 def destroy
105 @board.destroy
105 @board.destroy
106 redirect_to_settings_in_projects
106 redirect_to_settings_in_projects
107 end
107 end
108
108
109 private
109 private
110 def redirect_to_settings_in_projects
110 def redirect_to_settings_in_projects
111 redirect_to settings_project_path(@project, :tab => 'boards')
111 redirect_to settings_project_path(@project, :tab => 'boards')
112 end
112 end
113
113
114 def find_board_if_available
114 def find_board_if_available
115 @board = @project.boards.find(params[:id]) if params[:id]
115 @board = @project.boards.find(params[:id]) if params[:id]
116 rescue ActiveRecord::RecordNotFound
116 rescue ActiveRecord::RecordNotFound
117 render_404
117 render_404
118 end
118 end
119 end
119 end
@@ -1,96 +1,96
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 CustomFieldsController < ApplicationController
18 class CustomFieldsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_action :require_admin
21 before_action :require_admin
22 before_action :build_new_custom_field, :only => [:new, :create]
22 before_action :build_new_custom_field, :only => [:new, :create]
23 before_action :find_custom_field, :only => [:edit, :update, :destroy]
23 before_action :find_custom_field, :only => [:edit, :update, :destroy]
24 accept_api_auth :index
24 accept_api_auth :index
25
25
26 def index
26 def index
27 respond_to do |format|
27 respond_to do |format|
28 format.html {
28 format.html {
29 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
29 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
30 }
30 }
31 format.api {
31 format.api {
32 @custom_fields = CustomField.all
32 @custom_fields = CustomField.all
33 }
33 }
34 end
34 end
35 end
35 end
36
36
37 def new
37 def new
38 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
38 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
39 @custom_field.default_value = nil
39 @custom_field.default_value = nil
40 end
40 end
41
41
42 def create
42 def create
43 if @custom_field.save
43 if @custom_field.save
44 flash[:notice] = l(:notice_successful_create)
44 flash[:notice] = l(:notice_successful_create)
45 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
45 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
46 redirect_to edit_custom_field_path(@custom_field)
46 redirect_to edit_custom_field_path(@custom_field)
47 else
47 else
48 render :action => 'new'
48 render :action => 'new'
49 end
49 end
50 end
50 end
51
51
52 def edit
52 def edit
53 end
53 end
54
54
55 def update
55 def update
56 if @custom_field.update_attributes(params[:custom_field])
56 if @custom_field.update_attributes(params[:custom_field])
57 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
57 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
58 respond_to do |format|
58 respond_to do |format|
59 format.html {
59 format.html {
60 flash[:notice] = l(:notice_successful_update)
60 flash[:notice] = l(:notice_successful_update)
61 redirect_back_or_default edit_custom_field_path(@custom_field)
61 redirect_back_or_default edit_custom_field_path(@custom_field)
62 }
62 }
63 format.js { render :nothing => true }
63 format.js { head 200 }
64 end
64 end
65 else
65 else
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :action => 'edit' }
67 format.html { render :action => 'edit' }
68 format.js { render :nothing => true, :status => 422 }
68 format.js { head 422 }
69 end
69 end
70 end
70 end
71 end
71 end
72
72
73 def destroy
73 def destroy
74 begin
74 begin
75 @custom_field.destroy
75 @custom_field.destroy
76 rescue
76 rescue
77 flash[:error] = l(:error_can_not_delete_custom_field)
77 flash[:error] = l(:error_can_not_delete_custom_field)
78 end
78 end
79 redirect_to custom_fields_path(:tab => @custom_field.class.name)
79 redirect_to custom_fields_path(:tab => @custom_field.class.name)
80 end
80 end
81
81
82 private
82 private
83
83
84 def build_new_custom_field
84 def build_new_custom_field
85 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
85 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
86 if @custom_field.nil?
86 if @custom_field.nil?
87 render :action => 'select_type'
87 render :action => 'select_type'
88 end
88 end
89 end
89 end
90
90
91 def find_custom_field
91 def find_custom_field
92 @custom_field = CustomField.find(params[:id])
92 @custom_field = CustomField.find(params[:id])
93 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
94 render_404
94 render_404
95 end
95 end
96 end
96 end
@@ -1,104 +1,104
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 EnumerationsController < ApplicationController
18 class EnumerationsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_action :require_admin, :except => :index
21 before_action :require_admin, :except => :index
22 before_action :require_admin_or_api_request, :only => :index
22 before_action :require_admin_or_api_request, :only => :index
23 before_action :build_new_enumeration, :only => [:new, :create]
23 before_action :build_new_enumeration, :only => [:new, :create]
24 before_action :find_enumeration, :only => [:edit, :update, :destroy]
24 before_action :find_enumeration, :only => [:edit, :update, :destroy]
25 accept_api_auth :index
25 accept_api_auth :index
26
26
27 helper :custom_fields
27 helper :custom_fields
28
28
29 def index
29 def index
30 respond_to do |format|
30 respond_to do |format|
31 format.html
31 format.html
32 format.api {
32 format.api {
33 @klass = Enumeration.get_subclass(params[:type])
33 @klass = Enumeration.get_subclass(params[:type])
34 if @klass
34 if @klass
35 @enumerations = @klass.shared.sorted.to_a
35 @enumerations = @klass.shared.sorted.to_a
36 else
36 else
37 render_404
37 render_404
38 end
38 end
39 }
39 }
40 end
40 end
41 end
41 end
42
42
43 def new
43 def new
44 end
44 end
45
45
46 def create
46 def create
47 if request.post? && @enumeration.save
47 if request.post? && @enumeration.save
48 flash[:notice] = l(:notice_successful_create)
48 flash[:notice] = l(:notice_successful_create)
49 redirect_to enumerations_path
49 redirect_to enumerations_path
50 else
50 else
51 render :action => 'new'
51 render :action => 'new'
52 end
52 end
53 end
53 end
54
54
55 def edit
55 def edit
56 end
56 end
57
57
58 def update
58 def update
59 if @enumeration.update_attributes(params[:enumeration])
59 if @enumeration.update_attributes(params[:enumeration])
60 respond_to do |format|
60 respond_to do |format|
61 format.html {
61 format.html {
62 flash[:notice] = l(:notice_successful_update)
62 flash[:notice] = l(:notice_successful_update)
63 redirect_to enumerations_path
63 redirect_to enumerations_path
64 }
64 }
65 format.js { render :nothing => true }
65 format.js { head 200 }
66 end
66 end
67 else
67 else
68 respond_to do |format|
68 respond_to do |format|
69 format.html { render :action => 'edit' }
69 format.html { render :action => 'edit' }
70 format.js { render :nothing => true, :status => 422 }
70 format.js { head 422 }
71 end
71 end
72 end
72 end
73 end
73 end
74
74
75 def destroy
75 def destroy
76 if !@enumeration.in_use?
76 if !@enumeration.in_use?
77 # No associated objects
77 # No associated objects
78 @enumeration.destroy
78 @enumeration.destroy
79 redirect_to enumerations_path
79 redirect_to enumerations_path
80 return
80 return
81 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
81 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
82 @enumeration.destroy(reassign_to)
82 @enumeration.destroy(reassign_to)
83 redirect_to enumerations_path
83 redirect_to enumerations_path
84 return
84 return
85 end
85 end
86 @enumerations = @enumeration.class.system.to_a - [@enumeration]
86 @enumerations = @enumeration.class.system.to_a - [@enumeration]
87 end
87 end
88
88
89 private
89 private
90
90
91 def build_new_enumeration
91 def build_new_enumeration
92 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
92 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
93 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
93 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
94 if @enumeration.nil?
94 if @enumeration.nil?
95 render_404
95 render_404
96 end
96 end
97 end
97 end
98
98
99 def find_enumeration
99 def find_enumeration
100 @enumeration = Enumeration.find(params[:id])
100 @enumeration = Enumeration.find(params[:id])
101 rescue ActiveRecord::RecordNotFound
101 rescue ActiveRecord::RecordNotFound
102 render_404
102 render_404
103 end
103 end
104 end
104 end
@@ -1,90 +1,90
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 IssueRelationsController < ApplicationController
18 class IssueRelationsController < ApplicationController
19 before_action :find_issue, :authorize, :only => [:index, :create]
19 before_action :find_issue, :authorize, :only => [:index, :create]
20 before_action :find_relation, :only => [:show, :destroy]
20 before_action :find_relation, :only => [:show, :destroy]
21
21
22 accept_api_auth :index, :show, :create, :destroy
22 accept_api_auth :index, :show, :create, :destroy
23
23
24 def index
24 def index
25 @relations = @issue.relations
25 @relations = @issue.relations
26
26
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :nothing => true }
28 format.html { head 200 }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def show
33 def show
34 raise Unauthorized unless @relation.visible?
34 raise Unauthorized unless @relation.visible?
35
35
36 respond_to do |format|
36 respond_to do |format|
37 format.html { render :nothing => true }
37 format.html { head 200 }
38 format.api
38 format.api
39 end
39 end
40 end
40 end
41
41
42 def create
42 def create
43 @relation = IssueRelation.new
43 @relation = IssueRelation.new
44 @relation.issue_from = @issue
44 @relation.issue_from = @issue
45 @relation.safe_attributes = params[:relation]
45 @relation.safe_attributes = params[:relation]
46 @relation.init_journals(User.current)
46 @relation.init_journals(User.current)
47 saved = @relation.save
47 saved = @relation.save
48
48
49 respond_to do |format|
49 respond_to do |format|
50 format.html { redirect_to issue_path(@issue) }
50 format.html { redirect_to issue_path(@issue) }
51 format.js {
51 format.js {
52 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
52 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
53 }
53 }
54 format.api {
54 format.api {
55 if saved
55 if saved
56 render :action => 'show', :status => :created, :location => relation_url(@relation)
56 render :action => 'show', :status => :created, :location => relation_url(@relation)
57 else
57 else
58 render_validation_errors(@relation)
58 render_validation_errors(@relation)
59 end
59 end
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 def destroy
64 def destroy
65 raise Unauthorized unless @relation.deletable?
65 raise Unauthorized unless @relation.deletable?
66 @relation.init_journals(User.current)
66 @relation.init_journals(User.current)
67 @relation.destroy
67 @relation.destroy
68
68
69 respond_to do |format|
69 respond_to do |format|
70 format.html { redirect_to issue_path(@relation.issue_from) }
70 format.html { redirect_to issue_path(@relation.issue_from) }
71 format.js
71 format.js
72 format.api { render_api_ok }
72 format.api { render_api_ok }
73 end
73 end
74 end
74 end
75
75
76 private
76 private
77
77
78 def find_issue
78 def find_issue
79 @issue = Issue.find(params[:issue_id])
79 @issue = Issue.find(params[:issue_id])
80 @project = @issue.project
80 @project = @issue.project
81 rescue ActiveRecord::RecordNotFound
81 rescue ActiveRecord::RecordNotFound
82 render_404
82 render_404
83 end
83 end
84
84
85 def find_relation
85 def find_relation
86 @relation = IssueRelation.find(params[:id])
86 @relation = IssueRelation.find(params[:id])
87 rescue ActiveRecord::RecordNotFound
87 rescue ActiveRecord::RecordNotFound
88 render_404
88 render_404
89 end
89 end
90 end
90 end
@@ -1,85 +1,85
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 IssueStatusesController < ApplicationController
18 class IssueStatusesController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_action :require_admin, :except => :index
21 before_action :require_admin, :except => :index
22 before_action :require_admin_or_api_request, :only => :index
22 before_action :require_admin_or_api_request, :only => :index
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 def index
25 def index
26 @issue_statuses = IssueStatus.sorted.to_a
26 @issue_statuses = IssueStatus.sorted.to_a
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :layout => false if request.xhr? }
28 format.html { render :layout => false if request.xhr? }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def new
33 def new
34 @issue_status = IssueStatus.new
34 @issue_status = IssueStatus.new
35 end
35 end
36
36
37 def create
37 def create
38 @issue_status = IssueStatus.new(params[:issue_status])
38 @issue_status = IssueStatus.new(params[:issue_status])
39 if @issue_status.save
39 if @issue_status.save
40 flash[:notice] = l(:notice_successful_create)
40 flash[:notice] = l(:notice_successful_create)
41 redirect_to issue_statuses_path
41 redirect_to issue_statuses_path
42 else
42 else
43 render :action => 'new'
43 render :action => 'new'
44 end
44 end
45 end
45 end
46
46
47 def edit
47 def edit
48 @issue_status = IssueStatus.find(params[:id])
48 @issue_status = IssueStatus.find(params[:id])
49 end
49 end
50
50
51 def update
51 def update
52 @issue_status = IssueStatus.find(params[:id])
52 @issue_status = IssueStatus.find(params[:id])
53 if @issue_status.update_attributes(params[:issue_status])
53 if @issue_status.update_attributes(params[:issue_status])
54 respond_to do |format|
54 respond_to do |format|
55 format.html {
55 format.html {
56 flash[:notice] = l(:notice_successful_update)
56 flash[:notice] = l(:notice_successful_update)
57 redirect_to issue_statuses_path(:page => params[:page])
57 redirect_to issue_statuses_path(:page => params[:page])
58 }
58 }
59 format.js { render :nothing => true }
59 format.js { head 200 }
60 end
60 end
61 else
61 else
62 respond_to do |format|
62 respond_to do |format|
63 format.html { render :action => 'edit' }
63 format.html { render :action => 'edit' }
64 format.js { render :nothing => true, :status => 422 }
64 format.js { head 422 }
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 def destroy
69 def destroy
70 IssueStatus.find(params[:id]).destroy
70 IssueStatus.find(params[:id]).destroy
71 redirect_to issue_statuses_path
71 redirect_to issue_statuses_path
72 rescue
72 rescue
73 flash[:error] = l(:error_unable_delete_issue_status)
73 flash[:error] = l(:error_unable_delete_issue_status)
74 redirect_to issue_statuses_path
74 redirect_to issue_statuses_path
75 end
75 end
76
76
77 def update_issue_done_ratio
77 def update_issue_done_ratio
78 if request.post? && IssueStatus.update_issue_done_ratios
78 if request.post? && IssueStatus.update_issue_done_ratios
79 flash[:notice] = l(:notice_issue_done_ratios_updated)
79 flash[:notice] = l(:notice_issue_done_ratios_updated)
80 else
80 else
81 flash[:error] = l(:error_issue_done_ratios_not_updated)
81 flash[:error] = l(:error_issue_done_ratios_not_updated)
82 end
82 end
83 redirect_to issue_statuses_path
83 redirect_to issue_statuses_path
84 end
84 end
85 end
85 end
@@ -1,548 +1,548
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 default_search_scope :issues
19 default_search_scope :issues
20
20
21 before_action :find_issue, :only => [:show, :edit, :update]
21 before_action :find_issue, :only => [:show, :edit, :update]
22 before_action :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_action :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_action :authorize, :except => [:index, :new, :create]
23 before_action :authorize, :except => [:index, :new, :create]
24 before_action :find_optional_project, :only => [:index, :new, :create]
24 before_action :find_optional_project, :only => [:index, :new, :create]
25 before_action :build_new_issue_from_params, :only => [:new, :create]
25 before_action :build_new_issue_from_params, :only => [:new, :create]
26 accept_rss_auth :index, :show
26 accept_rss_auth :index, :show
27 accept_api_auth :index, :show, :create, :update, :destroy
27 accept_api_auth :index, :show, :create, :update, :destroy
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 helper :custom_fields
33 helper :custom_fields
34 helper :issue_relations
34 helper :issue_relations
35 helper :watchers
35 helper :watchers
36 helper :attachments
36 helper :attachments
37 helper :queries
37 helper :queries
38 include QueriesHelper
38 include QueriesHelper
39 helper :repositories
39 helper :repositories
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 helper :timelog
42 helper :timelog
43
43
44 def index
44 def index
45 retrieve_query
45 retrieve_query
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
47 sort_update(@query.sortable_columns)
47 sort_update(@query.sortable_columns)
48 @query.sort_criteria = sort_criteria.to_a
48 @query.sort_criteria = sort_criteria.to_a
49
49
50 if @query.valid?
50 if @query.valid?
51 case params[:format]
51 case params[:format]
52 when 'csv', 'pdf'
52 when 'csv', 'pdf'
53 @limit = Setting.issues_export_limit.to_i
53 @limit = Setting.issues_export_limit.to_i
54 if params[:columns] == 'all'
54 if params[:columns] == 'all'
55 @query.column_names = @query.available_inline_columns.map(&:name)
55 @query.column_names = @query.available_inline_columns.map(&:name)
56 end
56 end
57 when 'atom'
57 when 'atom'
58 @limit = Setting.feeds_limit.to_i
58 @limit = Setting.feeds_limit.to_i
59 when 'xml', 'json'
59 when 'xml', 'json'
60 @offset, @limit = api_offset_and_limit
60 @offset, @limit = api_offset_and_limit
61 @query.column_names = %w(author)
61 @query.column_names = %w(author)
62 else
62 else
63 @limit = per_page_option
63 @limit = per_page_option
64 end
64 end
65
65
66 @issue_count = @query.issue_count
66 @issue_count = @query.issue_count
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
68 @offset ||= @issue_pages.offset
68 @offset ||= @issue_pages.offset
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 :order => sort_clause,
70 :order => sort_clause,
71 :offset => @offset,
71 :offset => @offset,
72 :limit => @limit)
72 :limit => @limit)
73 @issue_count_by_group = @query.issue_count_by_group
73 @issue_count_by_group = @query.issue_count_by_group
74
74
75 respond_to do |format|
75 respond_to do |format|
76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
77 format.api {
77 format.api {
78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
79 }
79 }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
83 end
83 end
84 else
84 else
85 respond_to do |format|
85 respond_to do |format|
86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
87 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
87 format.any(:atom, :csv, :pdf) { head 422 }
88 format.api { render_validation_errors(@query) }
88 format.api { render_validation_errors(@query) }
89 end
89 end
90 end
90 end
91 rescue ActiveRecord::RecordNotFound
91 rescue ActiveRecord::RecordNotFound
92 render_404
92 render_404
93 end
93 end
94
94
95 def show
95 def show
96 @journals = @issue.journals.
96 @journals = @issue.journals.
97 preload(:details).
97 preload(:details).
98 preload(:user => :email_address).
98 preload(:user => :email_address).
99 reorder(:created_on, :id).to_a
99 reorder(:created_on, :id).to_a
100 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.each_with_index {|j,i| j.indice = i+1}
101 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
101 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
102 Journal.preload_journals_details_custom_fields(@journals)
102 Journal.preload_journals_details_custom_fields(@journals)
103 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
103 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105
105
106 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
106 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108
108
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @priorities = IssuePriority.active
111 @priorities = IssuePriority.active
112 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
112 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
113 @relation = IssueRelation.new
113 @relation = IssueRelation.new
114
114
115 respond_to do |format|
115 respond_to do |format|
116 format.html {
116 format.html {
117 retrieve_previous_and_next_issue_ids
117 retrieve_previous_and_next_issue_ids
118 render :template => 'issues/show'
118 render :template => 'issues/show'
119 }
119 }
120 format.api
120 format.api
121 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
121 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
122 format.pdf {
122 format.pdf {
123 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
123 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
124 }
124 }
125 end
125 end
126 end
126 end
127
127
128 def new
128 def new
129 respond_to do |format|
129 respond_to do |format|
130 format.html { render :action => 'new', :layout => !request.xhr? }
130 format.html { render :action => 'new', :layout => !request.xhr? }
131 format.js
131 format.js
132 end
132 end
133 end
133 end
134
134
135 def create
135 def create
136 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
136 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
137 raise ::Unauthorized
137 raise ::Unauthorized
138 end
138 end
139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
140 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
141 if @issue.save
141 if @issue.save
142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 respond_to do |format|
143 respond_to do |format|
144 format.html {
144 format.html {
145 render_attachment_warning_if_needed(@issue)
145 render_attachment_warning_if_needed(@issue)
146 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
146 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
147 redirect_after_create
147 redirect_after_create
148 }
148 }
149 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
149 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
150 end
150 end
151 return
151 return
152 else
152 else
153 respond_to do |format|
153 respond_to do |format|
154 format.html {
154 format.html {
155 if @issue.project.nil?
155 if @issue.project.nil?
156 render_error :status => 422
156 render_error :status => 422
157 else
157 else
158 render :action => 'new'
158 render :action => 'new'
159 end
159 end
160 }
160 }
161 format.api { render_validation_errors(@issue) }
161 format.api { render_validation_errors(@issue) }
162 end
162 end
163 end
163 end
164 end
164 end
165
165
166 def edit
166 def edit
167 return unless update_issue_from_params
167 return unless update_issue_from_params
168
168
169 respond_to do |format|
169 respond_to do |format|
170 format.html { }
170 format.html { }
171 format.js
171 format.js
172 end
172 end
173 end
173 end
174
174
175 def update
175 def update
176 return unless update_issue_from_params
176 return unless update_issue_from_params
177 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
177 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 saved = false
178 saved = false
179 begin
179 begin
180 saved = save_issue_with_child_records
180 saved = save_issue_with_child_records
181 rescue ActiveRecord::StaleObjectError
181 rescue ActiveRecord::StaleObjectError
182 @conflict = true
182 @conflict = true
183 if params[:last_journal_id]
183 if params[:last_journal_id]
184 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
184 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
185 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
185 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
186 end
186 end
187 end
187 end
188
188
189 if saved
189 if saved
190 render_attachment_warning_if_needed(@issue)
190 render_attachment_warning_if_needed(@issue)
191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192
192
193 respond_to do |format|
193 respond_to do |format|
194 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
194 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
195 format.api { render_api_ok }
195 format.api { render_api_ok }
196 end
196 end
197 else
197 else
198 respond_to do |format|
198 respond_to do |format|
199 format.html { render :action => 'edit' }
199 format.html { render :action => 'edit' }
200 format.api { render_validation_errors(@issue) }
200 format.api { render_validation_errors(@issue) }
201 end
201 end
202 end
202 end
203 end
203 end
204
204
205 # Bulk edit/copy a set of issues
205 # Bulk edit/copy a set of issues
206 def bulk_edit
206 def bulk_edit
207 @issues.sort!
207 @issues.sort!
208 @copy = params[:copy].present?
208 @copy = params[:copy].present?
209 @notes = params[:notes]
209 @notes = params[:notes]
210
210
211 if @copy
211 if @copy
212 unless User.current.allowed_to?(:copy_issues, @projects)
212 unless User.current.allowed_to?(:copy_issues, @projects)
213 raise ::Unauthorized
213 raise ::Unauthorized
214 end
214 end
215 else
215 else
216 unless @issues.all?(&:attributes_editable?)
216 unless @issues.all?(&:attributes_editable?)
217 raise ::Unauthorized
217 raise ::Unauthorized
218 end
218 end
219 end
219 end
220
220
221 @allowed_projects = Issue.allowed_target_projects
221 @allowed_projects = Issue.allowed_target_projects
222 if params[:issue]
222 if params[:issue]
223 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
223 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
224 if @target_project
224 if @target_project
225 target_projects = [@target_project]
225 target_projects = [@target_project]
226 end
226 end
227 end
227 end
228 target_projects ||= @projects
228 target_projects ||= @projects
229
229
230 if @copy
230 if @copy
231 # Copied issues will get their default statuses
231 # Copied issues will get their default statuses
232 @available_statuses = []
232 @available_statuses = []
233 else
233 else
234 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
234 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
235 end
235 end
236 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
236 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
237 @assignables = target_projects.map(&:assignable_users).reduce(:&)
237 @assignables = target_projects.map(&:assignable_users).reduce(:&)
238 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
238 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
239 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
239 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
240 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
240 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
241 if @copy
241 if @copy
242 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
242 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
243 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
243 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
244 end
244 end
245
245
246 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
246 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
247
247
248 @issue_params = params[:issue] || {}
248 @issue_params = params[:issue] || {}
249 @issue_params[:custom_field_values] ||= {}
249 @issue_params[:custom_field_values] ||= {}
250 end
250 end
251
251
252 def bulk_update
252 def bulk_update
253 @issues.sort!
253 @issues.sort!
254 @copy = params[:copy].present?
254 @copy = params[:copy].present?
255
255
256 attributes = parse_params_for_bulk_update(params[:issue])
256 attributes = parse_params_for_bulk_update(params[:issue])
257 copy_subtasks = (params[:copy_subtasks] == '1')
257 copy_subtasks = (params[:copy_subtasks] == '1')
258 copy_attachments = (params[:copy_attachments] == '1')
258 copy_attachments = (params[:copy_attachments] == '1')
259
259
260 if @copy
260 if @copy
261 unless User.current.allowed_to?(:copy_issues, @projects)
261 unless User.current.allowed_to?(:copy_issues, @projects)
262 raise ::Unauthorized
262 raise ::Unauthorized
263 end
263 end
264 target_projects = @projects
264 target_projects = @projects
265 if attributes['project_id'].present?
265 if attributes['project_id'].present?
266 target_projects = Project.where(:id => attributes['project_id']).to_a
266 target_projects = Project.where(:id => attributes['project_id']).to_a
267 end
267 end
268 unless User.current.allowed_to?(:add_issues, target_projects)
268 unless User.current.allowed_to?(:add_issues, target_projects)
269 raise ::Unauthorized
269 raise ::Unauthorized
270 end
270 end
271 else
271 else
272 unless @issues.all?(&:attributes_editable?)
272 unless @issues.all?(&:attributes_editable?)
273 raise ::Unauthorized
273 raise ::Unauthorized
274 end
274 end
275 end
275 end
276
276
277 unsaved_issues = []
277 unsaved_issues = []
278 saved_issues = []
278 saved_issues = []
279
279
280 if @copy && copy_subtasks
280 if @copy && copy_subtasks
281 # Descendant issues will be copied with the parent task
281 # Descendant issues will be copied with the parent task
282 # Don't copy them twice
282 # Don't copy them twice
283 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
283 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
284 end
284 end
285
285
286 @issues.each do |orig_issue|
286 @issues.each do |orig_issue|
287 orig_issue.reload
287 orig_issue.reload
288 if @copy
288 if @copy
289 issue = orig_issue.copy({},
289 issue = orig_issue.copy({},
290 :attachments => copy_attachments,
290 :attachments => copy_attachments,
291 :subtasks => copy_subtasks,
291 :subtasks => copy_subtasks,
292 :link => link_copy?(params[:link_copy])
292 :link => link_copy?(params[:link_copy])
293 )
293 )
294 else
294 else
295 issue = orig_issue
295 issue = orig_issue
296 end
296 end
297 journal = issue.init_journal(User.current, params[:notes])
297 journal = issue.init_journal(User.current, params[:notes])
298 issue.safe_attributes = attributes
298 issue.safe_attributes = attributes
299 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
299 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
300 if issue.save
300 if issue.save
301 saved_issues << issue
301 saved_issues << issue
302 else
302 else
303 unsaved_issues << orig_issue
303 unsaved_issues << orig_issue
304 end
304 end
305 end
305 end
306
306
307 if unsaved_issues.empty?
307 if unsaved_issues.empty?
308 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
308 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
309 if params[:follow]
309 if params[:follow]
310 if @issues.size == 1 && saved_issues.size == 1
310 if @issues.size == 1 && saved_issues.size == 1
311 redirect_to issue_path(saved_issues.first)
311 redirect_to issue_path(saved_issues.first)
312 elsif saved_issues.map(&:project).uniq.size == 1
312 elsif saved_issues.map(&:project).uniq.size == 1
313 redirect_to project_issues_path(saved_issues.map(&:project).first)
313 redirect_to project_issues_path(saved_issues.map(&:project).first)
314 end
314 end
315 else
315 else
316 redirect_back_or_default _project_issues_path(@project)
316 redirect_back_or_default _project_issues_path(@project)
317 end
317 end
318 else
318 else
319 @saved_issues = @issues
319 @saved_issues = @issues
320 @unsaved_issues = unsaved_issues
320 @unsaved_issues = unsaved_issues
321 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
321 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
322 bulk_edit
322 bulk_edit
323 render :action => 'bulk_edit'
323 render :action => 'bulk_edit'
324 end
324 end
325 end
325 end
326
326
327 def destroy
327 def destroy
328 raise Unauthorized unless @issues.all?(&:deletable?)
328 raise Unauthorized unless @issues.all?(&:deletable?)
329 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
329 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
330 if @hours > 0
330 if @hours > 0
331 case params[:todo]
331 case params[:todo]
332 when 'destroy'
332 when 'destroy'
333 # nothing to do
333 # nothing to do
334 when 'nullify'
334 when 'nullify'
335 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
335 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
336 when 'reassign'
336 when 'reassign'
337 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
337 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
338 if reassign_to.nil?
338 if reassign_to.nil?
339 flash.now[:error] = l(:error_issue_not_found_in_project)
339 flash.now[:error] = l(:error_issue_not_found_in_project)
340 return
340 return
341 else
341 else
342 TimeEntry.where(['issue_id IN (?)', @issues]).
342 TimeEntry.where(['issue_id IN (?)', @issues]).
343 update_all("issue_id = #{reassign_to.id}")
343 update_all("issue_id = #{reassign_to.id}")
344 end
344 end
345 else
345 else
346 # display the destroy form if it's a user request
346 # display the destroy form if it's a user request
347 return unless api_request?
347 return unless api_request?
348 end
348 end
349 end
349 end
350 @issues.each do |issue|
350 @issues.each do |issue|
351 begin
351 begin
352 issue.reload.destroy
352 issue.reload.destroy
353 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
353 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
354 # nothing to do, issue was already deleted (eg. by a parent)
354 # nothing to do, issue was already deleted (eg. by a parent)
355 end
355 end
356 end
356 end
357 respond_to do |format|
357 respond_to do |format|
358 format.html { redirect_back_or_default _project_issues_path(@project) }
358 format.html { redirect_back_or_default _project_issues_path(@project) }
359 format.api { render_api_ok }
359 format.api { render_api_ok }
360 end
360 end
361 end
361 end
362
362
363 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
363 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
364 # when the "New issue" tab is enabled
364 # when the "New issue" tab is enabled
365 def current_menu_item
365 def current_menu_item
366 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
366 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
367 :new_issue
367 :new_issue
368 else
368 else
369 super
369 super
370 end
370 end
371 end
371 end
372
372
373 private
373 private
374
374
375 def retrieve_previous_and_next_issue_ids
375 def retrieve_previous_and_next_issue_ids
376 if params[:prev_issue_id].present? || params[:next_issue_id].present?
376 if params[:prev_issue_id].present? || params[:next_issue_id].present?
377 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
377 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
378 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
378 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
379 @issue_position = params[:issue_position].presence.try(:to_i)
379 @issue_position = params[:issue_position].presence.try(:to_i)
380 @issue_count = params[:issue_count].presence.try(:to_i)
380 @issue_count = params[:issue_count].presence.try(:to_i)
381 else
381 else
382 retrieve_query_from_session
382 retrieve_query_from_session
383 if @query
383 if @query
384 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
384 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
385 sort_update(@query.sortable_columns, 'issues_index_sort')
385 sort_update(@query.sortable_columns, 'issues_index_sort')
386 limit = 500
386 limit = 500
387 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
387 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
388 if (idx = issue_ids.index(@issue.id)) && idx < limit
388 if (idx = issue_ids.index(@issue.id)) && idx < limit
389 if issue_ids.size < 500
389 if issue_ids.size < 500
390 @issue_position = idx + 1
390 @issue_position = idx + 1
391 @issue_count = issue_ids.size
391 @issue_count = issue_ids.size
392 end
392 end
393 @prev_issue_id = issue_ids[idx - 1] if idx > 0
393 @prev_issue_id = issue_ids[idx - 1] if idx > 0
394 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
394 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
395 end
395 end
396 end
396 end
397 end
397 end
398 end
398 end
399
399
400 def previous_and_next_issue_ids_params
400 def previous_and_next_issue_ids_params
401 {
401 {
402 :prev_issue_id => params[:prev_issue_id],
402 :prev_issue_id => params[:prev_issue_id],
403 :next_issue_id => params[:next_issue_id],
403 :next_issue_id => params[:next_issue_id],
404 :issue_position => params[:issue_position],
404 :issue_position => params[:issue_position],
405 :issue_count => params[:issue_count]
405 :issue_count => params[:issue_count]
406 }.reject {|k,v| k.blank?}
406 }.reject {|k,v| k.blank?}
407 end
407 end
408
408
409 # Used by #edit and #update to set some common instance variables
409 # Used by #edit and #update to set some common instance variables
410 # from the params
410 # from the params
411 def update_issue_from_params
411 def update_issue_from_params
412 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
412 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
413 if params[:time_entry]
413 if params[:time_entry]
414 @time_entry.safe_attributes = params[:time_entry]
414 @time_entry.safe_attributes = params[:time_entry]
415 end
415 end
416
416
417 @issue.init_journal(User.current)
417 @issue.init_journal(User.current)
418
418
419 issue_attributes = params[:issue]
419 issue_attributes = params[:issue]
420 if issue_attributes && params[:conflict_resolution]
420 if issue_attributes && params[:conflict_resolution]
421 case params[:conflict_resolution]
421 case params[:conflict_resolution]
422 when 'overwrite'
422 when 'overwrite'
423 issue_attributes = issue_attributes.dup
423 issue_attributes = issue_attributes.dup
424 issue_attributes.delete(:lock_version)
424 issue_attributes.delete(:lock_version)
425 when 'add_notes'
425 when 'add_notes'
426 issue_attributes = issue_attributes.slice(:notes, :private_notes)
426 issue_attributes = issue_attributes.slice(:notes, :private_notes)
427 when 'cancel'
427 when 'cancel'
428 redirect_to issue_path(@issue)
428 redirect_to issue_path(@issue)
429 return false
429 return false
430 end
430 end
431 end
431 end
432 @issue.safe_attributes = issue_attributes
432 @issue.safe_attributes = issue_attributes
433 @priorities = IssuePriority.active
433 @priorities = IssuePriority.active
434 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
434 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
435 true
435 true
436 end
436 end
437
437
438 # Used by #new and #create to build a new issue from the params
438 # Used by #new and #create to build a new issue from the params
439 # The new issue will be copied from an existing one if copy_from parameter is given
439 # The new issue will be copied from an existing one if copy_from parameter is given
440 def build_new_issue_from_params
440 def build_new_issue_from_params
441 @issue = Issue.new
441 @issue = Issue.new
442 if params[:copy_from]
442 if params[:copy_from]
443 begin
443 begin
444 @issue.init_journal(User.current)
444 @issue.init_journal(User.current)
445 @copy_from = Issue.visible.find(params[:copy_from])
445 @copy_from = Issue.visible.find(params[:copy_from])
446 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
446 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
447 raise ::Unauthorized
447 raise ::Unauthorized
448 end
448 end
449 @link_copy = link_copy?(params[:link_copy]) || request.get?
449 @link_copy = link_copy?(params[:link_copy]) || request.get?
450 @copy_attachments = params[:copy_attachments].present? || request.get?
450 @copy_attachments = params[:copy_attachments].present? || request.get?
451 @copy_subtasks = params[:copy_subtasks].present? || request.get?
451 @copy_subtasks = params[:copy_subtasks].present? || request.get?
452 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
452 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
453 @issue.parent_issue_id = @copy_from.parent_id
453 @issue.parent_issue_id = @copy_from.parent_id
454 rescue ActiveRecord::RecordNotFound
454 rescue ActiveRecord::RecordNotFound
455 render_404
455 render_404
456 return
456 return
457 end
457 end
458 end
458 end
459 @issue.project = @project
459 @issue.project = @project
460 if request.get?
460 if request.get?
461 @issue.project ||= @issue.allowed_target_projects.first
461 @issue.project ||= @issue.allowed_target_projects.first
462 end
462 end
463 @issue.author ||= User.current
463 @issue.author ||= User.current
464 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
464 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
465
465
466 attrs = (params[:issue] || {}).deep_dup
466 attrs = (params[:issue] || {}).deep_dup
467 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
467 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
468 attrs.delete(:status_id)
468 attrs.delete(:status_id)
469 end
469 end
470 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
470 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
471 # Discard submitted version when changing the project on the issue form
471 # Discard submitted version when changing the project on the issue form
472 # so we can use the default version for the new project
472 # so we can use the default version for the new project
473 attrs.delete(:fixed_version_id)
473 attrs.delete(:fixed_version_id)
474 end
474 end
475 @issue.safe_attributes = attrs
475 @issue.safe_attributes = attrs
476
476
477 if @issue.project
477 if @issue.project
478 @issue.tracker ||= @issue.allowed_target_trackers.first
478 @issue.tracker ||= @issue.allowed_target_trackers.first
479 if @issue.tracker.nil?
479 if @issue.tracker.nil?
480 if @issue.project.trackers.any?
480 if @issue.project.trackers.any?
481 # None of the project trackers is allowed to the user
481 # None of the project trackers is allowed to the user
482 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
482 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
483 else
483 else
484 # Project has no trackers
484 # Project has no trackers
485 render_error l(:error_no_tracker_in_project)
485 render_error l(:error_no_tracker_in_project)
486 end
486 end
487 return false
487 return false
488 end
488 end
489 if @issue.status.nil?
489 if @issue.status.nil?
490 render_error l(:error_no_default_issue_status)
490 render_error l(:error_no_default_issue_status)
491 return false
491 return false
492 end
492 end
493 end
493 end
494
494
495 @priorities = IssuePriority.active
495 @priorities = IssuePriority.active
496 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
496 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
497 end
497 end
498
498
499 # Saves @issue and a time_entry from the parameters
499 # Saves @issue and a time_entry from the parameters
500 def save_issue_with_child_records
500 def save_issue_with_child_records
501 Issue.transaction do
501 Issue.transaction do
502 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
502 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
503 time_entry = @time_entry || TimeEntry.new
503 time_entry = @time_entry || TimeEntry.new
504 time_entry.project = @issue.project
504 time_entry.project = @issue.project
505 time_entry.issue = @issue
505 time_entry.issue = @issue
506 time_entry.user = User.current
506 time_entry.user = User.current
507 time_entry.spent_on = User.current.today
507 time_entry.spent_on = User.current.today
508 time_entry.attributes = params[:time_entry]
508 time_entry.attributes = params[:time_entry]
509 @issue.time_entries << time_entry
509 @issue.time_entries << time_entry
510 end
510 end
511
511
512 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
512 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
513 if @issue.save
513 if @issue.save
514 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
514 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
515 else
515 else
516 raise ActiveRecord::Rollback
516 raise ActiveRecord::Rollback
517 end
517 end
518 end
518 end
519 end
519 end
520
520
521 # Returns true if the issue copy should be linked
521 # Returns true if the issue copy should be linked
522 # to the original issue
522 # to the original issue
523 def link_copy?(param)
523 def link_copy?(param)
524 case Setting.link_copied_issue
524 case Setting.link_copied_issue
525 when 'yes'
525 when 'yes'
526 true
526 true
527 when 'no'
527 when 'no'
528 false
528 false
529 when 'ask'
529 when 'ask'
530 param == '1'
530 param == '1'
531 end
531 end
532 end
532 end
533
533
534 # Redirects user after a successful issue creation
534 # Redirects user after a successful issue creation
535 def redirect_after_create
535 def redirect_after_create
536 if params[:continue]
536 if params[:continue]
537 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
537 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
538 if params[:project_id]
538 if params[:project_id]
539 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
539 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
540 else
540 else
541 attrs.merge! :project_id => @issue.project_id
541 attrs.merge! :project_id => @issue.project_id
542 redirect_to new_issue_path(:issue => attrs)
542 redirect_to new_issue_path(:issue => attrs)
543 end
543 end
544 else
544 else
545 redirect_to issue_path(@issue)
545 redirect_to issue_path(@issue)
546 end
546 end
547 end
547 end
548 end
548 end
@@ -1,44 +1,44
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 MailHandlerController < ActionController::Base
18 class MailHandlerController < ActionController::Base
19 before_action :check_credential
19 before_action :check_credential
20
20
21 # Displays the email submission form
21 # Displays the email submission form
22 def new
22 def new
23 end
23 end
24
24
25 # Submits an incoming email to MailHandler
25 # Submits an incoming email to MailHandler
26 def index
26 def index
27 options = params.dup
27 options = params.dup
28 email = options.delete(:email)
28 email = options.delete(:email)
29 if MailHandler.receive(email, options)
29 if MailHandler.receive(email, options)
30 render :nothing => true, :status => :created
30 head :created
31 else
31 else
32 render :nothing => true, :status => :unprocessable_entity
32 head :unprocessable_entity
33 end
33 end
34 end
34 end
35
35
36 private
36 private
37
37
38 def check_credential
38 def check_credential
39 User.current = nil
39 User.current = nil
40 unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
40 unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
41 render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
41 render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
42 end
42 end
43 end
43 end
44 end
44 end
@@ -1,211 +1,211
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 MyController < ApplicationController
18 class MyController < ApplicationController
19 before_action :require_login
19 before_action :require_login
20 # let user change user's password when user has to
20 # let user change user's password when user has to
21 skip_before_action :check_password_change, :only => :password
21 skip_before_action :check_password_change, :only => :password
22
22
23 require_sudo_mode :account, only: :post
23 require_sudo_mode :account, only: :post
24 require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
24 require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
25
25
26 helper :issues
26 helper :issues
27 helper :users
27 helper :users
28 helper :custom_fields
28 helper :custom_fields
29
29
30 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
30 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
31 'issuesreportedbyme' => :label_reported_issues,
31 'issuesreportedbyme' => :label_reported_issues,
32 'issueswatched' => :label_watched_issues,
32 'issueswatched' => :label_watched_issues,
33 'news' => :label_news_latest,
33 'news' => :label_news_latest,
34 'calendar' => :label_calendar,
34 'calendar' => :label_calendar,
35 'documents' => :label_document_plural,
35 'documents' => :label_document_plural,
36 'timelog' => :label_spent_time
36 'timelog' => :label_spent_time
37 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
37 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
38
38
39 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
39 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
40 'right' => ['issuesreportedbyme']
40 'right' => ['issuesreportedbyme']
41 }.freeze
41 }.freeze
42
42
43 def index
43 def index
44 page
44 page
45 render :action => 'page'
45 render :action => 'page'
46 end
46 end
47
47
48 # Show user's page
48 # Show user's page
49 def page
49 def page
50 @user = User.current
50 @user = User.current
51 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
51 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
52 end
52 end
53
53
54 # Edit user's account
54 # Edit user's account
55 def account
55 def account
56 @user = User.current
56 @user = User.current
57 @pref = @user.pref
57 @pref = @user.pref
58 if request.post?
58 if request.post?
59 @user.safe_attributes = params[:user] if params[:user]
59 @user.safe_attributes = params[:user] if params[:user]
60 @user.pref.attributes = params[:pref] if params[:pref]
60 @user.pref.attributes = params[:pref] if params[:pref]
61 if @user.save
61 if @user.save
62 @user.pref.save
62 @user.pref.save
63 set_language_if_valid @user.language
63 set_language_if_valid @user.language
64 flash[:notice] = l(:notice_account_updated)
64 flash[:notice] = l(:notice_account_updated)
65 redirect_to my_account_path
65 redirect_to my_account_path
66 return
66 return
67 end
67 end
68 end
68 end
69 end
69 end
70
70
71 # Destroys user's account
71 # Destroys user's account
72 def destroy
72 def destroy
73 @user = User.current
73 @user = User.current
74 unless @user.own_account_deletable?
74 unless @user.own_account_deletable?
75 redirect_to my_account_path
75 redirect_to my_account_path
76 return
76 return
77 end
77 end
78
78
79 if request.post? && params[:confirm]
79 if request.post? && params[:confirm]
80 @user.destroy
80 @user.destroy
81 if @user.destroyed?
81 if @user.destroyed?
82 logout_user
82 logout_user
83 flash[:notice] = l(:notice_account_deleted)
83 flash[:notice] = l(:notice_account_deleted)
84 end
84 end
85 redirect_to home_path
85 redirect_to home_path
86 end
86 end
87 end
87 end
88
88
89 # Manage user's password
89 # Manage user's password
90 def password
90 def password
91 @user = User.current
91 @user = User.current
92 unless @user.change_password_allowed?
92 unless @user.change_password_allowed?
93 flash[:error] = l(:notice_can_t_change_password)
93 flash[:error] = l(:notice_can_t_change_password)
94 redirect_to my_account_path
94 redirect_to my_account_path
95 return
95 return
96 end
96 end
97 if request.post?
97 if request.post?
98 if !@user.check_password?(params[:password])
98 if !@user.check_password?(params[:password])
99 flash.now[:error] = l(:notice_account_wrong_password)
99 flash.now[:error] = l(:notice_account_wrong_password)
100 elsif params[:password] == params[:new_password]
100 elsif params[:password] == params[:new_password]
101 flash.now[:error] = l(:notice_new_password_must_be_different)
101 flash.now[:error] = l(:notice_new_password_must_be_different)
102 else
102 else
103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
104 @user.must_change_passwd = false
104 @user.must_change_passwd = false
105 if @user.save
105 if @user.save
106 # The session token was destroyed by the password change, generate a new one
106 # The session token was destroyed by the password change, generate a new one
107 session[:tk] = @user.generate_session_token
107 session[:tk] = @user.generate_session_token
108 Mailer.password_updated(@user)
108 Mailer.password_updated(@user)
109 flash[:notice] = l(:notice_account_password_updated)
109 flash[:notice] = l(:notice_account_password_updated)
110 redirect_to my_account_path
110 redirect_to my_account_path
111 end
111 end
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 # Create a new feeds key
116 # Create a new feeds key
117 def reset_rss_key
117 def reset_rss_key
118 if request.post?
118 if request.post?
119 if User.current.rss_token
119 if User.current.rss_token
120 User.current.rss_token.destroy
120 User.current.rss_token.destroy
121 User.current.reload
121 User.current.reload
122 end
122 end
123 User.current.rss_key
123 User.current.rss_key
124 flash[:notice] = l(:notice_feeds_access_key_reseted)
124 flash[:notice] = l(:notice_feeds_access_key_reseted)
125 end
125 end
126 redirect_to my_account_path
126 redirect_to my_account_path
127 end
127 end
128
128
129 def show_api_key
129 def show_api_key
130 @user = User.current
130 @user = User.current
131 end
131 end
132
132
133 # Create a new API key
133 # Create a new API key
134 def reset_api_key
134 def reset_api_key
135 if request.post?
135 if request.post?
136 if User.current.api_token
136 if User.current.api_token
137 User.current.api_token.destroy
137 User.current.api_token.destroy
138 User.current.reload
138 User.current.reload
139 end
139 end
140 User.current.api_key
140 User.current.api_key
141 flash[:notice] = l(:notice_api_access_key_reseted)
141 flash[:notice] = l(:notice_api_access_key_reseted)
142 end
142 end
143 redirect_to my_account_path
143 redirect_to my_account_path
144 end
144 end
145
145
146 # User's page layout configuration
146 # User's page layout configuration
147 def page_layout
147 def page_layout
148 @user = User.current
148 @user = User.current
149 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
149 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
150 @block_options = []
150 @block_options = []
151 BLOCKS.each do |k, v|
151 BLOCKS.each do |k, v|
152 unless @blocks.values.flatten.include?(k)
152 unless @blocks.values.flatten.include?(k)
153 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
153 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
154 end
154 end
155 end
155 end
156 end
156 end
157
157
158 # Add a block to user's page
158 # Add a block to user's page
159 # The block is added on top of the page
159 # The block is added on top of the page
160 # params[:block] : id of the block to add
160 # params[:block] : id of the block to add
161 def add_block
161 def add_block
162 block = params[:block].to_s.underscore
162 block = params[:block].to_s.underscore
163 if block.present? && BLOCKS.key?(block)
163 if block.present? && BLOCKS.key?(block)
164 @user = User.current
164 @user = User.current
165 layout = @user.pref[:my_page_layout] || {}
165 layout = @user.pref[:my_page_layout] || {}
166 # remove if already present in a group
166 # remove if already present in a group
167 %w(top left right).each {|f| (layout[f] ||= []).delete block }
167 %w(top left right).each {|f| (layout[f] ||= []).delete block }
168 # add it on top
168 # add it on top
169 layout['top'].unshift block
169 layout['top'].unshift block
170 @user.pref[:my_page_layout] = layout
170 @user.pref[:my_page_layout] = layout
171 @user.pref.save
171 @user.pref.save
172 end
172 end
173 redirect_to my_page_layout_path
173 redirect_to my_page_layout_path
174 end
174 end
175
175
176 # Remove a block to user's page
176 # Remove a block to user's page
177 # params[:block] : id of the block to remove
177 # params[:block] : id of the block to remove
178 def remove_block
178 def remove_block
179 block = params[:block].to_s.underscore
179 block = params[:block].to_s.underscore
180 @user = User.current
180 @user = User.current
181 # remove block in all groups
181 # remove block in all groups
182 layout = @user.pref[:my_page_layout] || {}
182 layout = @user.pref[:my_page_layout] || {}
183 %w(top left right).each {|f| (layout[f] ||= []).delete block }
183 %w(top left right).each {|f| (layout[f] ||= []).delete block }
184 @user.pref[:my_page_layout] = layout
184 @user.pref[:my_page_layout] = layout
185 @user.pref.save
185 @user.pref.save
186 redirect_to my_page_layout_path
186 redirect_to my_page_layout_path
187 end
187 end
188
188
189 # Change blocks order on user's page
189 # Change blocks order on user's page
190 # params[:group] : group to order (top, left or right)
190 # params[:group] : group to order (top, left or right)
191 # params[:list-(top|left|right)] : array of block ids of the group
191 # params[:list-(top|left|right)] : array of block ids of the group
192 def order_blocks
192 def order_blocks
193 group = params[:group]
193 group = params[:group]
194 @user = User.current
194 @user = User.current
195 if group.is_a?(String)
195 if group.is_a?(String)
196 group_items = (params["blocks"] || []).collect(&:underscore)
196 group_items = (params["blocks"] || []).collect(&:underscore)
197 group_items.each {|s| s.sub!(/^block_/, '')}
197 group_items.each {|s| s.sub!(/^block_/, '')}
198 if group_items and group_items.is_a? Array
198 if group_items and group_items.is_a? Array
199 layout = @user.pref[:my_page_layout] || {}
199 layout = @user.pref[:my_page_layout] || {}
200 # remove group blocks if they are presents in other groups
200 # remove group blocks if they are presents in other groups
201 %w(top left right).each {|f|
201 %w(top left right).each {|f|
202 layout[f] = (layout[f] || []) - group_items
202 layout[f] = (layout[f] || []) - group_items
203 }
203 }
204 layout[group] = group_items
204 layout[group] = group_items
205 @user.pref[:my_page_layout] = layout
205 @user.pref[:my_page_layout] = layout
206 @user.pref.save
206 @user.pref.save
207 end
207 end
208 end
208 end
209 render :nothing => true
209 head 200
210 end
210 end
211 end
211 end
@@ -1,452 +1,452
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21 require 'redmine/scm/adapters'
21 require 'redmine/scm/adapters'
22
22
23 class ChangesetNotFound < Exception; end
23 class ChangesetNotFound < Exception; end
24 class InvalidRevisionParam < Exception; end
24 class InvalidRevisionParam < Exception; end
25
25
26 class RepositoriesController < ApplicationController
26 class RepositoriesController < ApplicationController
27 menu_item :repository
27 menu_item :repository
28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 default_search_scope :changesets
29 default_search_scope :changesets
30
30
31 before_action :find_project_by_project_id, :only => [:new, :create]
31 before_action :find_project_by_project_id, :only => [:new, :create]
32 before_action :find_repository, :only => [:edit, :update, :destroy, :committers]
32 before_action :find_repository, :only => [:edit, :update, :destroy, :committers]
33 before_action :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
33 before_action :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 before_action :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
34 before_action :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 before_action :authorize
35 before_action :authorize
36 accept_rss_auth :revisions
36 accept_rss_auth :revisions
37
37
38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39
39
40 def new
40 def new
41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
42 @repository = Repository.factory(scm)
42 @repository = Repository.factory(scm)
43 @repository.is_default = @project.repository.nil?
43 @repository.is_default = @project.repository.nil?
44 @repository.project = @project
44 @repository.project = @project
45 end
45 end
46
46
47 def create
47 def create
48 attrs = pickup_extra_info
48 attrs = pickup_extra_info
49 @repository = Repository.factory(params[:repository_scm])
49 @repository = Repository.factory(params[:repository_scm])
50 @repository.safe_attributes = params[:repository]
50 @repository.safe_attributes = params[:repository]
51 if attrs[:attrs_extra].keys.any?
51 if attrs[:attrs_extra].keys.any?
52 @repository.merge_extra_info(attrs[:attrs_extra])
52 @repository.merge_extra_info(attrs[:attrs_extra])
53 end
53 end
54 @repository.project = @project
54 @repository.project = @project
55 if request.post? && @repository.save
55 if request.post? && @repository.save
56 redirect_to settings_project_path(@project, :tab => 'repositories')
56 redirect_to settings_project_path(@project, :tab => 'repositories')
57 else
57 else
58 render :action => 'new'
58 render :action => 'new'
59 end
59 end
60 end
60 end
61
61
62 def edit
62 def edit
63 end
63 end
64
64
65 def update
65 def update
66 attrs = pickup_extra_info
66 attrs = pickup_extra_info
67 @repository.safe_attributes = attrs[:attrs]
67 @repository.safe_attributes = attrs[:attrs]
68 if attrs[:attrs_extra].keys.any?
68 if attrs[:attrs_extra].keys.any?
69 @repository.merge_extra_info(attrs[:attrs_extra])
69 @repository.merge_extra_info(attrs[:attrs_extra])
70 end
70 end
71 @repository.project = @project
71 @repository.project = @project
72 if @repository.save
72 if @repository.save
73 redirect_to settings_project_path(@project, :tab => 'repositories')
73 redirect_to settings_project_path(@project, :tab => 'repositories')
74 else
74 else
75 render :action => 'edit'
75 render :action => 'edit'
76 end
76 end
77 end
77 end
78
78
79 def pickup_extra_info
79 def pickup_extra_info
80 p = {}
80 p = {}
81 p_extra = {}
81 p_extra = {}
82 params[:repository].each do |k, v|
82 params[:repository].each do |k, v|
83 if k =~ /^extra_/
83 if k =~ /^extra_/
84 p_extra[k] = v
84 p_extra[k] = v
85 else
85 else
86 p[k] = v
86 p[k] = v
87 end
87 end
88 end
88 end
89 {:attrs => p, :attrs_extra => p_extra}
89 {:attrs => p, :attrs_extra => p_extra}
90 end
90 end
91 private :pickup_extra_info
91 private :pickup_extra_info
92
92
93 def committers
93 def committers
94 @committers = @repository.committers
94 @committers = @repository.committers
95 @users = @project.users.to_a
95 @users = @project.users.to_a
96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 @users += User.where(:id => additional_user_ids).to_a unless additional_user_ids.empty?
97 @users += User.where(:id => additional_user_ids).to_a unless additional_user_ids.empty?
98 @users.compact!
98 @users.compact!
99 @users.sort!
99 @users.sort!
100 if request.post? && params[:committers].is_a?(Hash)
100 if request.post? && params[:committers].is_a?(Hash)
101 # Build a hash with repository usernames as keys and corresponding user ids as values
101 # Build a hash with repository usernames as keys and corresponding user ids as values
102 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
102 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
103 flash[:notice] = l(:notice_successful_update)
103 flash[:notice] = l(:notice_successful_update)
104 redirect_to settings_project_path(@project, :tab => 'repositories')
104 redirect_to settings_project_path(@project, :tab => 'repositories')
105 end
105 end
106 end
106 end
107
107
108 def destroy
108 def destroy
109 @repository.destroy if request.delete?
109 @repository.destroy if request.delete?
110 redirect_to settings_project_path(@project, :tab => 'repositories')
110 redirect_to settings_project_path(@project, :tab => 'repositories')
111 end
111 end
112
112
113 def show
113 def show
114 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
114 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
115
115
116 @entries = @repository.entries(@path, @rev)
116 @entries = @repository.entries(@path, @rev)
117 @changeset = @repository.find_changeset_by_name(@rev)
117 @changeset = @repository.find_changeset_by_name(@rev)
118 if request.xhr?
118 if request.xhr?
119 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
119 @entries ? render(:partial => 'dir_list_content') : head(200)
120 else
120 else
121 (show_error_not_found; return) unless @entries
121 (show_error_not_found; return) unless @entries
122 @changesets = @repository.latest_changesets(@path, @rev)
122 @changesets = @repository.latest_changesets(@path, @rev)
123 @properties = @repository.properties(@path, @rev)
123 @properties = @repository.properties(@path, @rev)
124 @repositories = @project.repositories
124 @repositories = @project.repositories
125 render :action => 'show'
125 render :action => 'show'
126 end
126 end
127 end
127 end
128
128
129 alias_method :browse, :show
129 alias_method :browse, :show
130
130
131 def changes
131 def changes
132 @entry = @repository.entry(@path, @rev)
132 @entry = @repository.entry(@path, @rev)
133 (show_error_not_found; return) unless @entry
133 (show_error_not_found; return) unless @entry
134 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
134 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
135 @properties = @repository.properties(@path, @rev)
135 @properties = @repository.properties(@path, @rev)
136 @changeset = @repository.find_changeset_by_name(@rev)
136 @changeset = @repository.find_changeset_by_name(@rev)
137 end
137 end
138
138
139 def revisions
139 def revisions
140 @changeset_count = @repository.changesets.count
140 @changeset_count = @repository.changesets.count
141 @changeset_pages = Paginator.new @changeset_count,
141 @changeset_pages = Paginator.new @changeset_count,
142 per_page_option,
142 per_page_option,
143 params['page']
143 params['page']
144 @changesets = @repository.changesets.
144 @changesets = @repository.changesets.
145 limit(@changeset_pages.per_page).
145 limit(@changeset_pages.per_page).
146 offset(@changeset_pages.offset).
146 offset(@changeset_pages.offset).
147 includes(:user, :repository, :parents).
147 includes(:user, :repository, :parents).
148 to_a
148 to_a
149
149
150 respond_to do |format|
150 respond_to do |format|
151 format.html { render :layout => false if request.xhr? }
151 format.html { render :layout => false if request.xhr? }
152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
153 end
153 end
154 end
154 end
155
155
156 def raw
156 def raw
157 entry_and_raw(true)
157 entry_and_raw(true)
158 end
158 end
159
159
160 def entry
160 def entry
161 entry_and_raw(false)
161 entry_and_raw(false)
162 end
162 end
163
163
164 def entry_and_raw(is_raw)
164 def entry_and_raw(is_raw)
165 @entry = @repository.entry(@path, @rev)
165 @entry = @repository.entry(@path, @rev)
166 (show_error_not_found; return) unless @entry
166 (show_error_not_found; return) unless @entry
167
167
168 # If the entry is a dir, show the browser
168 # If the entry is a dir, show the browser
169 (show; return) if @entry.is_dir?
169 (show; return) if @entry.is_dir?
170
170
171 if is_raw
171 if is_raw
172 # Force the download
172 # Force the download
173 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
173 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
174 send_type = Redmine::MimeType.of(@path)
174 send_type = Redmine::MimeType.of(@path)
175 send_opt[:type] = send_type.to_s if send_type
175 send_opt[:type] = send_type.to_s if send_type
176 send_opt[:disposition] = disposition(@path)
176 send_opt[:disposition] = disposition(@path)
177 send_data @repository.cat(@path, @rev), send_opt
177 send_data @repository.cat(@path, @rev), send_opt
178 else
178 else
179 if !@entry.size || @entry.size <= Setting.file_max_size_displayed.to_i.kilobyte
179 if !@entry.size || @entry.size <= Setting.file_max_size_displayed.to_i.kilobyte
180 content = @repository.cat(@path, @rev)
180 content = @repository.cat(@path, @rev)
181 (show_error_not_found; return) unless content
181 (show_error_not_found; return) unless content
182
182
183 if content.size <= Setting.file_max_size_displayed.to_i.kilobyte &&
183 if content.size <= Setting.file_max_size_displayed.to_i.kilobyte &&
184 is_entry_text_data?(content, @path)
184 is_entry_text_data?(content, @path)
185 # TODO: UTF-16
185 # TODO: UTF-16
186 # Prevent empty lines when displaying a file with Windows style eol
186 # Prevent empty lines when displaying a file with Windows style eol
187 # Is this needed? AttachmentsController simply reads file.
187 # Is this needed? AttachmentsController simply reads file.
188 @content = content.gsub("\r\n", "\n")
188 @content = content.gsub("\r\n", "\n")
189 end
189 end
190 end
190 end
191 @changeset = @repository.find_changeset_by_name(@rev)
191 @changeset = @repository.find_changeset_by_name(@rev)
192 end
192 end
193 end
193 end
194 private :entry_and_raw
194 private :entry_and_raw
195
195
196 def is_entry_text_data?(ent, path)
196 def is_entry_text_data?(ent, path)
197 # UTF-16 contains "\x00".
197 # UTF-16 contains "\x00".
198 # It is very strict that file contains less than 30% of ascii symbols
198 # It is very strict that file contains less than 30% of ascii symbols
199 # in non Western Europe.
199 # in non Western Europe.
200 return true if Redmine::MimeType.is_type?('text', path)
200 return true if Redmine::MimeType.is_type?('text', path)
201 # Ruby 1.8.6 has a bug of integer divisions.
201 # Ruby 1.8.6 has a bug of integer divisions.
202 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
202 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
203 return false if ent.is_binary_data?
203 return false if ent.is_binary_data?
204 true
204 true
205 end
205 end
206 private :is_entry_text_data?
206 private :is_entry_text_data?
207
207
208 def annotate
208 def annotate
209 @entry = @repository.entry(@path, @rev)
209 @entry = @repository.entry(@path, @rev)
210 (show_error_not_found; return) unless @entry
210 (show_error_not_found; return) unless @entry
211
211
212 @annotate = @repository.scm.annotate(@path, @rev)
212 @annotate = @repository.scm.annotate(@path, @rev)
213 if @annotate.nil? || @annotate.empty?
213 if @annotate.nil? || @annotate.empty?
214 (render_error l(:error_scm_annotate); return)
214 (render_error l(:error_scm_annotate); return)
215 end
215 end
216 ann_buf_size = 0
216 ann_buf_size = 0
217 @annotate.lines.each do |buf|
217 @annotate.lines.each do |buf|
218 ann_buf_size += buf.size
218 ann_buf_size += buf.size
219 end
219 end
220 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
220 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
221 (render_error l(:error_scm_annotate_big_text_file); return)
221 (render_error l(:error_scm_annotate_big_text_file); return)
222 end
222 end
223 @changeset = @repository.find_changeset_by_name(@rev)
223 @changeset = @repository.find_changeset_by_name(@rev)
224 end
224 end
225
225
226 def revision
226 def revision
227 respond_to do |format|
227 respond_to do |format|
228 format.html
228 format.html
229 format.js {render :layout => false}
229 format.js {render :layout => false}
230 end
230 end
231 end
231 end
232
232
233 # Adds a related issue to a changeset
233 # Adds a related issue to a changeset
234 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
234 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
235 def add_related_issue
235 def add_related_issue
236 issue_id = params[:issue_id].to_s.sub(/^#/,'')
236 issue_id = params[:issue_id].to_s.sub(/^#/,'')
237 @issue = @changeset.find_referenced_issue_by_id(issue_id)
237 @issue = @changeset.find_referenced_issue_by_id(issue_id)
238 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
238 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
239 @issue = nil
239 @issue = nil
240 end
240 end
241
241
242 if @issue
242 if @issue
243 @changeset.issues << @issue
243 @changeset.issues << @issue
244 end
244 end
245 end
245 end
246
246
247 # Removes a related issue from a changeset
247 # Removes a related issue from a changeset
248 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
248 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
249 def remove_related_issue
249 def remove_related_issue
250 @issue = Issue.visible.find_by_id(params[:issue_id])
250 @issue = Issue.visible.find_by_id(params[:issue_id])
251 if @issue
251 if @issue
252 @changeset.issues.delete(@issue)
252 @changeset.issues.delete(@issue)
253 end
253 end
254 end
254 end
255
255
256 def diff
256 def diff
257 if params[:format] == 'diff'
257 if params[:format] == 'diff'
258 @diff = @repository.diff(@path, @rev, @rev_to)
258 @diff = @repository.diff(@path, @rev, @rev_to)
259 (show_error_not_found; return) unless @diff
259 (show_error_not_found; return) unless @diff
260 filename = "changeset_r#{@rev}"
260 filename = "changeset_r#{@rev}"
261 filename << "_r#{@rev_to}" if @rev_to
261 filename << "_r#{@rev_to}" if @rev_to
262 send_data @diff.join, :filename => "#{filename}.diff",
262 send_data @diff.join, :filename => "#{filename}.diff",
263 :type => 'text/x-patch',
263 :type => 'text/x-patch',
264 :disposition => 'attachment'
264 :disposition => 'attachment'
265 else
265 else
266 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
266 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
267 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
267 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
268
268
269 # Save diff type as user preference
269 # Save diff type as user preference
270 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
270 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
271 User.current.pref[:diff_type] = @diff_type
271 User.current.pref[:diff_type] = @diff_type
272 User.current.preference.save
272 User.current.preference.save
273 end
273 end
274 @cache_key = "repositories/diff/#{@repository.id}/" +
274 @cache_key = "repositories/diff/#{@repository.id}/" +
275 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
275 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
276 unless read_fragment(@cache_key)
276 unless read_fragment(@cache_key)
277 @diff = @repository.diff(@path, @rev, @rev_to)
277 @diff = @repository.diff(@path, @rev, @rev_to)
278 show_error_not_found unless @diff
278 show_error_not_found unless @diff
279 end
279 end
280
280
281 @changeset = @repository.find_changeset_by_name(@rev)
281 @changeset = @repository.find_changeset_by_name(@rev)
282 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
282 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
283 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
283 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
284 end
284 end
285 end
285 end
286
286
287 def stats
287 def stats
288 end
288 end
289
289
290 def graph
290 def graph
291 data = nil
291 data = nil
292 case params[:graph]
292 case params[:graph]
293 when "commits_per_month"
293 when "commits_per_month"
294 data = graph_commits_per_month(@repository)
294 data = graph_commits_per_month(@repository)
295 when "commits_per_author"
295 when "commits_per_author"
296 data = graph_commits_per_author(@repository)
296 data = graph_commits_per_author(@repository)
297 end
297 end
298 if data
298 if data
299 headers["Content-Type"] = "image/svg+xml"
299 headers["Content-Type"] = "image/svg+xml"
300 send_data(data, :type => "image/svg+xml", :disposition => "inline")
300 send_data(data, :type => "image/svg+xml", :disposition => "inline")
301 else
301 else
302 render_404
302 render_404
303 end
303 end
304 end
304 end
305
305
306 private
306 private
307
307
308 def find_repository
308 def find_repository
309 @repository = Repository.find(params[:id])
309 @repository = Repository.find(params[:id])
310 @project = @repository.project
310 @project = @repository.project
311 rescue ActiveRecord::RecordNotFound
311 rescue ActiveRecord::RecordNotFound
312 render_404
312 render_404
313 end
313 end
314
314
315 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
315 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
316
316
317 def find_project_repository
317 def find_project_repository
318 @project = Project.find(params[:id])
318 @project = Project.find(params[:id])
319 if params[:repository_id].present?
319 if params[:repository_id].present?
320 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
320 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
321 else
321 else
322 @repository = @project.repository
322 @repository = @project.repository
323 end
323 end
324 (render_404; return false) unless @repository
324 (render_404; return false) unless @repository
325 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
325 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
326 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
326 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
327 @rev_to = params[:rev_to]
327 @rev_to = params[:rev_to]
328
328
329 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
329 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
330 if @repository.branches.blank?
330 if @repository.branches.blank?
331 raise InvalidRevisionParam
331 raise InvalidRevisionParam
332 end
332 end
333 end
333 end
334 rescue ActiveRecord::RecordNotFound
334 rescue ActiveRecord::RecordNotFound
335 render_404
335 render_404
336 rescue InvalidRevisionParam
336 rescue InvalidRevisionParam
337 show_error_not_found
337 show_error_not_found
338 end
338 end
339
339
340 def find_changeset
340 def find_changeset
341 if @rev.present?
341 if @rev.present?
342 @changeset = @repository.find_changeset_by_name(@rev)
342 @changeset = @repository.find_changeset_by_name(@rev)
343 end
343 end
344 show_error_not_found unless @changeset
344 show_error_not_found unless @changeset
345 end
345 end
346
346
347 def show_error_not_found
347 def show_error_not_found
348 render_error :message => l(:error_scm_not_found), :status => 404
348 render_error :message => l(:error_scm_not_found), :status => 404
349 end
349 end
350
350
351 # Handler for Redmine::Scm::Adapters::CommandFailed exception
351 # Handler for Redmine::Scm::Adapters::CommandFailed exception
352 def show_error_command_failed(exception)
352 def show_error_command_failed(exception)
353 render_error l(:error_scm_command_failed, exception.message)
353 render_error l(:error_scm_command_failed, exception.message)
354 end
354 end
355
355
356 def graph_commits_per_month(repository)
356 def graph_commits_per_month(repository)
357 @date_to = User.current.today
357 @date_to = User.current.today
358 @date_from = @date_to << 11
358 @date_from = @date_to << 11
359 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
359 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
360 commits_by_day = Changeset.
360 commits_by_day = Changeset.
361 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
361 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
362 group(:commit_date).
362 group(:commit_date).
363 count
363 count
364 commits_by_month = [0] * 12
364 commits_by_month = [0] * 12
365 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
365 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
366
366
367 changes_by_day = Change.
367 changes_by_day = Change.
368 joins(:changeset).
368 joins(:changeset).
369 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
369 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
370 group(:commit_date).
370 group(:commit_date).
371 count
371 count
372 changes_by_month = [0] * 12
372 changes_by_month = [0] * 12
373 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
373 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
374
374
375 fields = []
375 fields = []
376 today = User.current.today
376 today = User.current.today
377 12.times {|m| fields << month_name(((today.month - 1 - m) % 12) + 1)}
377 12.times {|m| fields << month_name(((today.month - 1 - m) % 12) + 1)}
378
378
379 graph = SVG::Graph::Bar.new(
379 graph = SVG::Graph::Bar.new(
380 :height => 300,
380 :height => 300,
381 :width => 800,
381 :width => 800,
382 :fields => fields.reverse,
382 :fields => fields.reverse,
383 :stack => :side,
383 :stack => :side,
384 :scale_integers => true,
384 :scale_integers => true,
385 :step_x_labels => 2,
385 :step_x_labels => 2,
386 :show_data_values => false,
386 :show_data_values => false,
387 :graph_title => l(:label_commits_per_month),
387 :graph_title => l(:label_commits_per_month),
388 :show_graph_title => true
388 :show_graph_title => true
389 )
389 )
390
390
391 graph.add_data(
391 graph.add_data(
392 :data => commits_by_month[0..11].reverse,
392 :data => commits_by_month[0..11].reverse,
393 :title => l(:label_revision_plural)
393 :title => l(:label_revision_plural)
394 )
394 )
395
395
396 graph.add_data(
396 graph.add_data(
397 :data => changes_by_month[0..11].reverse,
397 :data => changes_by_month[0..11].reverse,
398 :title => l(:label_change_plural)
398 :title => l(:label_change_plural)
399 )
399 )
400
400
401 graph.burn
401 graph.burn
402 end
402 end
403
403
404 def graph_commits_per_author(repository)
404 def graph_commits_per_author(repository)
405 #data
405 #data
406 stats = repository.stats_by_author
406 stats = repository.stats_by_author
407 fields, commits_data, changes_data = [], [], []
407 fields, commits_data, changes_data = [], [], []
408 stats.each do |name, hsh|
408 stats.each do |name, hsh|
409 fields << name
409 fields << name
410 commits_data << hsh[:commits_count]
410 commits_data << hsh[:commits_count]
411 changes_data << hsh[:changes_count]
411 changes_data << hsh[:changes_count]
412 end
412 end
413
413
414 #expand to 10 values if needed
414 #expand to 10 values if needed
415 fields = fields + [""]*(10 - fields.length) if fields.length<10
415 fields = fields + [""]*(10 - fields.length) if fields.length<10
416 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
416 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
417 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
417 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
418
418
419 # Remove email address in usernames
419 # Remove email address in usernames
420 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
420 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
421
421
422 #prepare graph
422 #prepare graph
423 graph = SVG::Graph::BarHorizontal.new(
423 graph = SVG::Graph::BarHorizontal.new(
424 :height => 30 * commits_data.length,
424 :height => 30 * commits_data.length,
425 :width => 800,
425 :width => 800,
426 :fields => fields,
426 :fields => fields,
427 :stack => :side,
427 :stack => :side,
428 :scale_integers => true,
428 :scale_integers => true,
429 :show_data_values => false,
429 :show_data_values => false,
430 :rotate_y_labels => false,
430 :rotate_y_labels => false,
431 :graph_title => l(:label_commits_per_author),
431 :graph_title => l(:label_commits_per_author),
432 :show_graph_title => true
432 :show_graph_title => true
433 )
433 )
434 graph.add_data(
434 graph.add_data(
435 :data => commits_data,
435 :data => commits_data,
436 :title => l(:label_revision_plural)
436 :title => l(:label_revision_plural)
437 )
437 )
438 graph.add_data(
438 graph.add_data(
439 :data => changes_data,
439 :data => changes_data,
440 :title => l(:label_change_plural)
440 :title => l(:label_change_plural)
441 )
441 )
442 graph.burn
442 graph.burn
443 end
443 end
444
444
445 def disposition(path)
445 def disposition(path)
446 if Redmine::MimeType.is_type?('image', @path) || Redmine::MimeType.of(@path) == "application/pdf"
446 if Redmine::MimeType.is_type?('image', @path) || Redmine::MimeType.of(@path) == "application/pdf"
447 'inline'
447 'inline'
448 else
448 else
449 'attachment'
449 'attachment'
450 end
450 end
451 end
451 end
452 end
452 end
@@ -1,121 +1,121
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 RolesController < ApplicationController
18 class RolesController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_action :require_admin, :except => [:index, :show]
21 before_action :require_admin, :except => [:index, :show]
22 before_action :require_admin_or_api_request, :only => [:index, :show]
22 before_action :require_admin_or_api_request, :only => [:index, :show]
23 before_action :find_role, :only => [:show, :edit, :update, :destroy]
23 before_action :find_role, :only => [:show, :edit, :update, :destroy]
24 accept_api_auth :index, :show
24 accept_api_auth :index, :show
25
25
26 require_sudo_mode :create, :update, :destroy
26 require_sudo_mode :create, :update, :destroy
27
27
28 def index
28 def index
29 respond_to do |format|
29 respond_to do |format|
30 format.html {
30 format.html {
31 @roles = Role.sorted.to_a
31 @roles = Role.sorted.to_a
32 render :layout => false if request.xhr?
32 render :layout => false if request.xhr?
33 }
33 }
34 format.api {
34 format.api {
35 @roles = Role.givable.to_a
35 @roles = Role.givable.to_a
36 }
36 }
37 end
37 end
38 end
38 end
39
39
40 def show
40 def show
41 respond_to do |format|
41 respond_to do |format|
42 format.api
42 format.api
43 end
43 end
44 end
44 end
45
45
46 def new
46 def new
47 # Prefills the form with 'Non member' role permissions by default
47 # Prefills the form with 'Non member' role permissions by default
48 @role = Role.new
48 @role = Role.new
49 @role.safe_attributes = params[:role] || {:permissions => Role.non_member.permissions}
49 @role.safe_attributes = params[:role] || {:permissions => Role.non_member.permissions}
50 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
50 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
51 @role.copy_from(@copy_from)
51 @role.copy_from(@copy_from)
52 end
52 end
53 @roles = Role.sorted.to_a
53 @roles = Role.sorted.to_a
54 end
54 end
55
55
56 def create
56 def create
57 @role = Role.new
57 @role = Role.new
58 @role.safe_attributes = params[:role]
58 @role.safe_attributes = params[:role]
59 if request.post? && @role.save
59 if request.post? && @role.save
60 # workflow copy
60 # workflow copy
61 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
61 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
62 @role.workflow_rules.copy(copy_from)
62 @role.workflow_rules.copy(copy_from)
63 end
63 end
64 flash[:notice] = l(:notice_successful_create)
64 flash[:notice] = l(:notice_successful_create)
65 redirect_to roles_path
65 redirect_to roles_path
66 else
66 else
67 @roles = Role.sorted.to_a
67 @roles = Role.sorted.to_a
68 render :action => 'new'
68 render :action => 'new'
69 end
69 end
70 end
70 end
71
71
72 def edit
72 def edit
73 end
73 end
74
74
75 def update
75 def update
76 @role.safe_attributes = params[:role]
76 @role.safe_attributes = params[:role]
77 if @role.save
77 if @role.save
78 respond_to do |format|
78 respond_to do |format|
79 format.html {
79 format.html {
80 flash[:notice] = l(:notice_successful_update)
80 flash[:notice] = l(:notice_successful_update)
81 redirect_to roles_path(:page => params[:page])
81 redirect_to roles_path(:page => params[:page])
82 }
82 }
83 format.js { render :nothing => true }
83 format.js { head 200 }
84 end
84 end
85 else
85 else
86 respond_to do |format|
86 respond_to do |format|
87 format.html { render :action => 'edit' }
87 format.html { render :action => 'edit' }
88 format.js { render :nothing => true, :status => 422 }
88 format.js { head 422 }
89 end
89 end
90 end
90 end
91 end
91 end
92
92
93 def destroy
93 def destroy
94 @role.destroy
94 @role.destroy
95 redirect_to roles_path
95 redirect_to roles_path
96 rescue
96 rescue
97 flash[:error] = l(:error_can_not_remove_role)
97 flash[:error] = l(:error_can_not_remove_role)
98 redirect_to roles_path
98 redirect_to roles_path
99 end
99 end
100
100
101 def permissions
101 def permissions
102 @roles = Role.sorted.to_a
102 @roles = Role.sorted.to_a
103 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
103 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
104 if request.post?
104 if request.post?
105 @roles.each do |role|
105 @roles.each do |role|
106 role.permissions = params[:permissions][role.id.to_s]
106 role.permissions = params[:permissions][role.id.to_s]
107 role.save
107 role.save
108 end
108 end
109 flash[:notice] = l(:notice_successful_update)
109 flash[:notice] = l(:notice_successful_update)
110 redirect_to roles_path
110 redirect_to roles_path
111 end
111 end
112 end
112 end
113
113
114 private
114 private
115
115
116 def find_role
116 def find_role
117 @role = Role.find(params[:id])
117 @role = Role.find(params[:id])
118 rescue ActiveRecord::RecordNotFound
118 rescue ActiveRecord::RecordNotFound
119 render_404
119 render_404
120 end
120 end
121 end
121 end
@@ -1,81 +1,81
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 SysController < ActionController::Base
18 class SysController < ActionController::Base
19 before_action :check_enabled
19 before_action :check_enabled
20
20
21 def projects
21 def projects
22 p = Project.active.has_module(:repository).
22 p = Project.active.has_module(:repository).
23 order("#{Project.table_name}.identifier").preload(:repository).to_a
23 order("#{Project.table_name}.identifier").preload(:repository).to_a
24 # extra_info attribute from repository breaks activeresource client
24 # extra_info attribute from repository breaks activeresource client
25 render :xml => p.to_xml(
25 render :xml => p.to_xml(
26 :only => [:id, :identifier, :name, :is_public, :status],
26 :only => [:id, :identifier, :name, :is_public, :status],
27 :include => {:repository => {:only => [:id, :url]}}
27 :include => {:repository => {:only => [:id, :url]}}
28 )
28 )
29 end
29 end
30
30
31 def create_project_repository
31 def create_project_repository
32 project = Project.find(params[:id])
32 project = Project.find(params[:id])
33 if project.repository
33 if project.repository
34 render :nothing => true, :status => 409
34 head 409
35 else
35 else
36 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
36 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
37 repository = Repository.factory(params[:vendor], params[:repository])
37 repository = Repository.factory(params[:vendor], params[:repository])
38 repository.project = project
38 repository.project = project
39 if repository.save
39 if repository.save
40 render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
40 render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
41 else
41 else
42 render :nothing => true, :status => 422
42 head 422
43 end
43 end
44 end
44 end
45 end
45 end
46
46
47 def fetch_changesets
47 def fetch_changesets
48 projects = []
48 projects = []
49 scope = Project.active.has_module(:repository)
49 scope = Project.active.has_module(:repository)
50 if params[:id]
50 if params[:id]
51 project = nil
51 project = nil
52 if params[:id].to_s =~ /^\d*$/
52 if params[:id].to_s =~ /^\d*$/
53 project = scope.find(params[:id])
53 project = scope.find(params[:id])
54 else
54 else
55 project = scope.find_by_identifier(params[:id])
55 project = scope.find_by_identifier(params[:id])
56 end
56 end
57 raise ActiveRecord::RecordNotFound unless project
57 raise ActiveRecord::RecordNotFound unless project
58 projects << project
58 projects << project
59 else
59 else
60 projects = scope.to_a
60 projects = scope.to_a
61 end
61 end
62 projects.each do |project|
62 projects.each do |project|
63 project.repositories.each do |repository|
63 project.repositories.each do |repository|
64 repository.fetch_changesets
64 repository.fetch_changesets
65 end
65 end
66 end
66 end
67 render :nothing => true, :status => 200
67 head 200
68 rescue ActiveRecord::RecordNotFound
68 rescue ActiveRecord::RecordNotFound
69 render :nothing => true, :status => 404
69 head 404
70 end
70 end
71
71
72 protected
72 protected
73
73
74 def check_enabled
74 def check_enabled
75 User.current = nil
75 User.current = nil
76 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
76 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
77 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
77 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
78 return false
78 return false
79 end
79 end
80 end
80 end
81 end
81 end
@@ -1,279 +1,279
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 before_action :find_time_entry, :only => [:show, :edit, :update]
21 before_action :find_time_entry, :only => [:show, :edit, :update]
22 before_action :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_action :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_action :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
23 before_action :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24
24
25 before_action :find_optional_issue, :only => [:new, :create]
25 before_action :find_optional_issue, :only => [:new, :create]
26 before_action :find_optional_project, :only => [:index, :report]
26 before_action :find_optional_project, :only => [:index, :report]
27 before_action :authorize_global, :only => [:new, :create, :index, :report]
27 before_action :authorize_global, :only => [:new, :create, :index, :report]
28
28
29 accept_rss_auth :index
29 accept_rss_auth :index
30 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :sort
34 helper :sort
35 include SortHelper
35 include SortHelper
36 helper :issues
36 helper :issues
37 include TimelogHelper
37 include TimelogHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :queries
40 helper :queries
41 include QueriesHelper
41 include QueriesHelper
42
42
43 def index
43 def index
44 retrieve_time_entry_query
44 retrieve_time_entry_query
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
46 sort_update(@query.sortable_columns)
46 sort_update(@query.sortable_columns)
47 scope = time_entry_scope(:order => sort_clause).
47 scope = time_entry_scope(:order => sort_clause).
48 includes(:project, :user, :issue).
48 includes(:project, :user, :issue).
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
50
50
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @entry_count = scope.count
53 @entry_count = scope.count
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
56
56
57 render :layout => !request.xhr?
57 render :layout => !request.xhr?
58 }
58 }
59 format.api {
59 format.api {
60 @entry_count = scope.count
60 @entry_count = scope.count
61 @offset, @limit = api_offset_and_limit
61 @offset, @limit = api_offset_and_limit
62 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
62 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
63 }
63 }
64 format.atom {
64 format.atom {
65 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
65 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
66 render_feed(entries, :title => l(:label_spent_time))
66 render_feed(entries, :title => l(:label_spent_time))
67 }
67 }
68 format.csv {
68 format.csv {
69 # Export all entries
69 # Export all entries
70 @entries = scope.to_a
70 @entries = scope.to_a
71 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
71 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
72 }
72 }
73 end
73 end
74 end
74 end
75
75
76 def report
76 def report
77 retrieve_time_entry_query
77 retrieve_time_entry_query
78 scope = time_entry_scope
78 scope = time_entry_scope
79
79
80 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
80 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
81
81
82 respond_to do |format|
82 respond_to do |format|
83 format.html { render :layout => !request.xhr? }
83 format.html { render :layout => !request.xhr? }
84 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
84 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
85 end
85 end
86 end
86 end
87
87
88 def show
88 def show
89 respond_to do |format|
89 respond_to do |format|
90 # TODO: Implement html response
90 # TODO: Implement html response
91 format.html { render :nothing => true, :status => 406 }
91 format.html { head 406 }
92 format.api
92 format.api
93 end
93 end
94 end
94 end
95
95
96 def new
96 def new
97 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
97 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
98 @time_entry.safe_attributes = params[:time_entry]
98 @time_entry.safe_attributes = params[:time_entry]
99 end
99 end
100
100
101 def create
101 def create
102 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
102 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
103 @time_entry.safe_attributes = params[:time_entry]
103 @time_entry.safe_attributes = params[:time_entry]
104 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
104 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
105 render_403
105 render_403
106 return
106 return
107 end
107 end
108
108
109 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
109 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110
110
111 if @time_entry.save
111 if @time_entry.save
112 respond_to do |format|
112 respond_to do |format|
113 format.html {
113 format.html {
114 flash[:notice] = l(:notice_successful_create)
114 flash[:notice] = l(:notice_successful_create)
115 if params[:continue]
115 if params[:continue]
116 options = {
116 options = {
117 :time_entry => {
117 :time_entry => {
118 :project_id => params[:time_entry][:project_id],
118 :project_id => params[:time_entry][:project_id],
119 :issue_id => @time_entry.issue_id,
119 :issue_id => @time_entry.issue_id,
120 :activity_id => @time_entry.activity_id
120 :activity_id => @time_entry.activity_id
121 },
121 },
122 :back_url => params[:back_url]
122 :back_url => params[:back_url]
123 }
123 }
124 if params[:project_id] && @time_entry.project
124 if params[:project_id] && @time_entry.project
125 redirect_to new_project_time_entry_path(@time_entry.project, options)
125 redirect_to new_project_time_entry_path(@time_entry.project, options)
126 elsif params[:issue_id] && @time_entry.issue
126 elsif params[:issue_id] && @time_entry.issue
127 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
127 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
128 else
128 else
129 redirect_to new_time_entry_path(options)
129 redirect_to new_time_entry_path(options)
130 end
130 end
131 else
131 else
132 redirect_back_or_default project_time_entries_path(@time_entry.project)
132 redirect_back_or_default project_time_entries_path(@time_entry.project)
133 end
133 end
134 }
134 }
135 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
135 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
136 end
136 end
137 else
137 else
138 respond_to do |format|
138 respond_to do |format|
139 format.html { render :action => 'new' }
139 format.html { render :action => 'new' }
140 format.api { render_validation_errors(@time_entry) }
140 format.api { render_validation_errors(@time_entry) }
141 end
141 end
142 end
142 end
143 end
143 end
144
144
145 def edit
145 def edit
146 @time_entry.safe_attributes = params[:time_entry]
146 @time_entry.safe_attributes = params[:time_entry]
147 end
147 end
148
148
149 def update
149 def update
150 @time_entry.safe_attributes = params[:time_entry]
150 @time_entry.safe_attributes = params[:time_entry]
151
151
152 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
152 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
153
153
154 if @time_entry.save
154 if @time_entry.save
155 respond_to do |format|
155 respond_to do |format|
156 format.html {
156 format.html {
157 flash[:notice] = l(:notice_successful_update)
157 flash[:notice] = l(:notice_successful_update)
158 redirect_back_or_default project_time_entries_path(@time_entry.project)
158 redirect_back_or_default project_time_entries_path(@time_entry.project)
159 }
159 }
160 format.api { render_api_ok }
160 format.api { render_api_ok }
161 end
161 end
162 else
162 else
163 respond_to do |format|
163 respond_to do |format|
164 format.html { render :action => 'edit' }
164 format.html { render :action => 'edit' }
165 format.api { render_validation_errors(@time_entry) }
165 format.api { render_validation_errors(@time_entry) }
166 end
166 end
167 end
167 end
168 end
168 end
169
169
170 def bulk_edit
170 def bulk_edit
171 @available_activities = TimeEntryActivity.shared.active
171 @available_activities = TimeEntryActivity.shared.active
172 @custom_fields = TimeEntry.first.available_custom_fields
172 @custom_fields = TimeEntry.first.available_custom_fields
173 end
173 end
174
174
175 def bulk_update
175 def bulk_update
176 attributes = parse_params_for_bulk_update(params[:time_entry])
176 attributes = parse_params_for_bulk_update(params[:time_entry])
177
177
178 unsaved_time_entry_ids = []
178 unsaved_time_entry_ids = []
179 @time_entries.each do |time_entry|
179 @time_entries.each do |time_entry|
180 time_entry.reload
180 time_entry.reload
181 time_entry.safe_attributes = attributes
181 time_entry.safe_attributes = attributes
182 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
182 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
183 unless time_entry.save
183 unless time_entry.save
184 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
184 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
185 # Keep unsaved time_entry ids to display them in flash error
185 # Keep unsaved time_entry ids to display them in flash error
186 unsaved_time_entry_ids << time_entry.id
186 unsaved_time_entry_ids << time_entry.id
187 end
187 end
188 end
188 end
189 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
189 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
190 redirect_back_or_default project_time_entries_path(@projects.first)
190 redirect_back_or_default project_time_entries_path(@projects.first)
191 end
191 end
192
192
193 def destroy
193 def destroy
194 destroyed = TimeEntry.transaction do
194 destroyed = TimeEntry.transaction do
195 @time_entries.each do |t|
195 @time_entries.each do |t|
196 unless t.destroy && t.destroyed?
196 unless t.destroy && t.destroyed?
197 raise ActiveRecord::Rollback
197 raise ActiveRecord::Rollback
198 end
198 end
199 end
199 end
200 end
200 end
201
201
202 respond_to do |format|
202 respond_to do |format|
203 format.html {
203 format.html {
204 if destroyed
204 if destroyed
205 flash[:notice] = l(:notice_successful_delete)
205 flash[:notice] = l(:notice_successful_delete)
206 else
206 else
207 flash[:error] = l(:notice_unable_delete_time_entry)
207 flash[:error] = l(:notice_unable_delete_time_entry)
208 end
208 end
209 redirect_back_or_default project_time_entries_path(@projects.first)
209 redirect_back_or_default project_time_entries_path(@projects.first)
210 }
210 }
211 format.api {
211 format.api {
212 if destroyed
212 if destroyed
213 render_api_ok
213 render_api_ok
214 else
214 else
215 render_validation_errors(@time_entries)
215 render_validation_errors(@time_entries)
216 end
216 end
217 }
217 }
218 end
218 end
219 end
219 end
220
220
221 private
221 private
222 def find_time_entry
222 def find_time_entry
223 @time_entry = TimeEntry.find(params[:id])
223 @time_entry = TimeEntry.find(params[:id])
224 unless @time_entry.editable_by?(User.current)
224 unless @time_entry.editable_by?(User.current)
225 render_403
225 render_403
226 return false
226 return false
227 end
227 end
228 @project = @time_entry.project
228 @project = @time_entry.project
229 rescue ActiveRecord::RecordNotFound
229 rescue ActiveRecord::RecordNotFound
230 render_404
230 render_404
231 end
231 end
232
232
233 def find_time_entries
233 def find_time_entries
234 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
234 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
235 raise ActiveRecord::RecordNotFound if @time_entries.empty?
235 raise ActiveRecord::RecordNotFound if @time_entries.empty?
236 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
236 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
237 @projects = @time_entries.collect(&:project).compact.uniq
237 @projects = @time_entries.collect(&:project).compact.uniq
238 @project = @projects.first if @projects.size == 1
238 @project = @projects.first if @projects.size == 1
239 rescue ActiveRecord::RecordNotFound
239 rescue ActiveRecord::RecordNotFound
240 render_404
240 render_404
241 end
241 end
242
242
243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 if unsaved_time_entry_ids.empty?
244 if unsaved_time_entry_ids.empty?
245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 else
246 else
247 flash[:error] = l(:notice_failed_to_save_time_entries,
247 flash[:error] = l(:notice_failed_to_save_time_entries,
248 :count => unsaved_time_entry_ids.size,
248 :count => unsaved_time_entry_ids.size,
249 :total => time_entries.size,
249 :total => time_entries.size,
250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 end
251 end
252 end
252 end
253
253
254 def find_optional_issue
254 def find_optional_issue
255 if params[:issue_id].present?
255 if params[:issue_id].present?
256 @issue = Issue.find(params[:issue_id])
256 @issue = Issue.find(params[:issue_id])
257 @project = @issue.project
257 @project = @issue.project
258 else
258 else
259 find_optional_project
259 find_optional_project
260 end
260 end
261 end
261 end
262
262
263 def find_optional_project
263 def find_optional_project
264 if params[:project_id].present?
264 if params[:project_id].present?
265 @project = Project.find(params[:project_id])
265 @project = Project.find(params[:project_id])
266 end
266 end
267 rescue ActiveRecord::RecordNotFound
267 rescue ActiveRecord::RecordNotFound
268 render_404
268 render_404
269 end
269 end
270
270
271 # Returns the TimeEntry scope for index and report actions
271 # Returns the TimeEntry scope for index and report actions
272 def time_entry_scope(options={})
272 def time_entry_scope(options={})
273 @query.results_scope(options)
273 @query.results_scope(options)
274 end
274 end
275
275
276 def retrieve_time_entry_query
276 def retrieve_time_entry_query
277 retrieve_query(TimeEntryQuery, false)
277 retrieve_query(TimeEntryQuery, false)
278 end
278 end
279 end
279 end
@@ -1,110 +1,110
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TrackersController < ApplicationController
18 class TrackersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_action :require_admin, :except => :index
21 before_action :require_admin, :except => :index
22 before_action :require_admin_or_api_request, :only => :index
22 before_action :require_admin_or_api_request, :only => :index
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 def index
25 def index
26 @trackers = Tracker.sorted.to_a
26 @trackers = Tracker.sorted.to_a
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :layout => false if request.xhr? }
28 format.html { render :layout => false if request.xhr? }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def new
33 def new
34 @tracker ||= Tracker.new
34 @tracker ||= Tracker.new
35 @tracker.safe_attributes = params[:tracker]
35 @tracker.safe_attributes = params[:tracker]
36 @trackers = Tracker.sorted.to_a
36 @trackers = Tracker.sorted.to_a
37 @projects = Project.all
37 @projects = Project.all
38 end
38 end
39
39
40 def create
40 def create
41 @tracker = Tracker.new
41 @tracker = Tracker.new
42 @tracker.safe_attributes = params[:tracker]
42 @tracker.safe_attributes = params[:tracker]
43 if @tracker.save
43 if @tracker.save
44 # workflow copy
44 # workflow copy
45 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
45 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
46 @tracker.workflow_rules.copy(copy_from)
46 @tracker.workflow_rules.copy(copy_from)
47 end
47 end
48 flash[:notice] = l(:notice_successful_create)
48 flash[:notice] = l(:notice_successful_create)
49 redirect_to trackers_path
49 redirect_to trackers_path
50 return
50 return
51 end
51 end
52 new
52 new
53 render :action => 'new'
53 render :action => 'new'
54 end
54 end
55
55
56 def edit
56 def edit
57 @tracker ||= Tracker.find(params[:id])
57 @tracker ||= Tracker.find(params[:id])
58 @projects = Project.all
58 @projects = Project.all
59 end
59 end
60
60
61 def update
61 def update
62 @tracker = Tracker.find(params[:id])
62 @tracker = Tracker.find(params[:id])
63 @tracker.safe_attributes = params[:tracker]
63 @tracker.safe_attributes = params[:tracker]
64 if @tracker.save
64 if @tracker.save
65 respond_to do |format|
65 respond_to do |format|
66 format.html {
66 format.html {
67 flash[:notice] = l(:notice_successful_update)
67 flash[:notice] = l(:notice_successful_update)
68 redirect_to trackers_path(:page => params[:page])
68 redirect_to trackers_path(:page => params[:page])
69 }
69 }
70 format.js { render :nothing => true }
70 format.js { head 200 }
71 end
71 end
72 else
72 else
73 respond_to do |format|
73 respond_to do |format|
74 format.html {
74 format.html {
75 edit
75 edit
76 render :action => 'edit'
76 render :action => 'edit'
77 }
77 }
78 format.js { render :nothing => true, :status => 422 }
78 format.js { head 422 }
79 end
79 end
80 end
80 end
81 end
81 end
82
82
83 def destroy
83 def destroy
84 @tracker = Tracker.find(params[:id])
84 @tracker = Tracker.find(params[:id])
85 unless @tracker.issues.empty?
85 unless @tracker.issues.empty?
86 flash[:error] = l(:error_can_not_delete_tracker)
86 flash[:error] = l(:error_can_not_delete_tracker)
87 else
87 else
88 @tracker.destroy
88 @tracker.destroy
89 end
89 end
90 redirect_to trackers_path
90 redirect_to trackers_path
91 end
91 end
92
92
93 def fields
93 def fields
94 if request.post? && params[:trackers]
94 if request.post? && params[:trackers]
95 params[:trackers].each do |tracker_id, tracker_params|
95 params[:trackers].each do |tracker_id, tracker_params|
96 tracker = Tracker.find_by_id(tracker_id)
96 tracker = Tracker.find_by_id(tracker_id)
97 if tracker
97 if tracker
98 tracker.core_fields = tracker_params[:core_fields]
98 tracker.core_fields = tracker_params[:core_fields]
99 tracker.custom_field_ids = tracker_params[:custom_field_ids]
99 tracker.custom_field_ids = tracker_params[:custom_field_ids]
100 tracker.save
100 tracker.save
101 end
101 end
102 end
102 end
103 flash[:notice] = l(:notice_successful_update)
103 flash[:notice] = l(:notice_successful_update)
104 redirect_to fields_trackers_path
104 redirect_to fields_trackers_path
105 return
105 return
106 end
106 end
107 @trackers = Tracker.sorted.to_a
107 @trackers = Tracker.sorted.to_a
108 @custom_fields = IssueCustomField.all.sort
108 @custom_fields = IssueCustomField.all.sort
109 end
109 end
110 end
110 end
General Comments 0
You need to be logged in to leave comments. Login now