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