##// END OF EJS Templates
Merged r15238....
Jean-Philippe Lang -
r14857:b2131b32453a
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,359 +1,359
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 AccountController < ApplicationController
19 19 helper :custom_fields
20 20 include CustomFieldsHelper
21 21
22 22 # prevents login action to be filtered by check_if_login_required application scope filter
23 23 skip_before_filter :check_if_login_required, :check_password_change
24 24
25 25 # Overrides ApplicationController#verify_authenticity_token to disable
26 26 # token verification on openid callbacks
27 27 def verify_authenticity_token
28 28 unless using_open_id?
29 29 super
30 30 end
31 31 end
32 32
33 33 # Login request and validation
34 34 def login
35 35 if request.get?
36 36 if User.current.logged?
37 37 redirect_back_or_default home_url, :referer => true
38 38 end
39 39 else
40 40 authenticate_user
41 41 end
42 42 rescue AuthSourceException => e
43 43 logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
44 44 render_error :message => e.message
45 45 end
46 46
47 47 # Log out current user and redirect to welcome page
48 48 def logout
49 49 if User.current.anonymous?
50 50 redirect_to home_url
51 51 elsif request.post?
52 52 logout_user
53 53 redirect_to home_url
54 54 end
55 55 # display the logout form
56 56 end
57 57
58 58 # Lets user choose a new password
59 59 def lost_password
60 60 (redirect_to(home_url); return) unless Setting.lost_password?
61 61 if params[:token]
62 62 @token = Token.find_token("recovery", params[:token].to_s)
63 63 if @token.nil? || @token.expired?
64 64 redirect_to home_url
65 65 return
66 66 end
67 67 @user = @token.user
68 68 unless @user && @user.active?
69 69 redirect_to home_url
70 70 return
71 71 end
72 72 if request.post?
73 73 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
74 74 if @user.save
75 75 @token.destroy
76 76 flash[:notice] = l(:notice_account_password_updated)
77 77 redirect_to signin_path
78 78 return
79 79 end
80 80 end
81 81 render :template => "account/password_recovery"
82 82 return
83 83 else
84 84 if request.post?
85 85 email = params[:mail].to_s
86 86 user = User.find_by_mail(email)
87 87 # user not found
88 88 unless user
89 89 flash.now[:error] = l(:notice_account_unknown_email)
90 90 return
91 91 end
92 92 unless user.active?
93 93 handle_inactive_user(user, lost_password_path)
94 94 return
95 95 end
96 96 # user cannot change its password
97 97 unless user.change_password_allowed?
98 98 flash.now[:error] = l(:notice_can_t_change_password)
99 99 return
100 100 end
101 101 # create a new token for password recovery
102 102 token = Token.new(:user => user, :action => "recovery")
103 103 if token.save
104 104 # Don't use the param to send the email
105 105 recipent = user.mails.detect {|e| email.casecmp(e) == 0} || user.mail
106 106 Mailer.lost_password(token, recipent).deliver
107 107 flash[:notice] = l(:notice_account_lost_email_sent)
108 108 redirect_to signin_path
109 109 return
110 110 end
111 111 end
112 112 end
113 113 end
114 114
115 115 # User self-registration
116 116 def register
117 117 (redirect_to(home_url); return) unless Setting.self_registration? || session[:auth_source_registration]
118 118 if request.get?
119 119 session[:auth_source_registration] = nil
120 120 @user = User.new(:language => current_language.to_s)
121 121 else
122 122 user_params = params[:user] || {}
123 123 @user = User.new
124 124 @user.safe_attributes = user_params
125 125 @user.admin = false
126 126 @user.register
127 127 if session[:auth_source_registration]
128 128 @user.activate
129 129 @user.login = session[:auth_source_registration][:login]
130 130 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
131 131 if @user.save
132 132 session[:auth_source_registration] = nil
133 133 self.logged_user = @user
134 134 flash[:notice] = l(:notice_account_activated)
135 135 redirect_to my_account_path
136 136 end
137 137 else
138 138 @user.login = params[:user][:login]
139 139 unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
140 140 @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
141 141 end
142 142
143 143 case Setting.self_registration
144 144 when '1'
145 145 register_by_email_activation(@user)
146 146 when '3'
147 147 register_automatically(@user)
148 148 else
149 149 register_manually_by_administrator(@user)
150 150 end
151 151 end
152 152 end
153 153 end
154 154
155 155 # Token based account activation
156 156 def activate
157 157 (redirect_to(home_url); return) unless Setting.self_registration? && params[:token].present?
158 158 token = Token.find_token('register', params[:token].to_s)
159 159 (redirect_to(home_url); return) unless token and !token.expired?
160 160 user = token.user
161 161 (redirect_to(home_url); return) unless user.registered?
162 162 user.activate
163 163 if user.save
164 164 token.destroy
165 165 flash[:notice] = l(:notice_account_activated)
166 166 end
167 167 redirect_to signin_path
168 168 end
169 169
170 170 # Sends a new account activation email
171 171 def activation_email
172 172 if session[:registered_user_id] && Setting.self_registration == '1'
173 173 user_id = session.delete(:registered_user_id).to_i
174 174 user = User.find_by_id(user_id)
175 175 if user && user.registered?
176 176 register_by_email_activation(user)
177 177 return
178 178 end
179 179 end
180 180 redirect_to(home_url)
181 181 end
182 182
183 183 private
184 184
185 185 def authenticate_user
186 186 if Setting.openid? && using_open_id?
187 187 open_id_authenticate(params[:openid_url])
188 188 else
189 189 password_authentication
190 190 end
191 191 end
192 192
193 193 def password_authentication
194 194 user = User.try_to_login(params[:username], params[:password], false)
195 195
196 196 if user.nil?
197 197 invalid_credentials
198 198 elsif user.new_record?
199 199 onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
200 200 else
201 201 # Valid user
202 202 if user.active?
203 203 successful_authentication(user)
204 204 update_sudo_timestamp! # activate Sudo Mode
205 205 else
206 206 handle_inactive_user(user)
207 207 end
208 208 end
209 209 end
210 210
211 211 def open_id_authenticate(openid_url)
212 212 back_url = signin_url(:autologin => params[:autologin])
213 213 authenticate_with_open_id(
214 214 openid_url, :required => [:nickname, :fullname, :email],
215 215 :return_to => back_url, :method => :post
216 216 ) do |result, identity_url, registration|
217 217 if result.successful?
218 218 user = User.find_or_initialize_by_identity_url(identity_url)
219 219 if user.new_record?
220 220 # Self-registration off
221 221 (redirect_to(home_url); return) unless Setting.self_registration?
222 222 # Create on the fly
223 223 user.login = registration['nickname'] unless registration['nickname'].nil?
224 224 user.mail = registration['email'] unless registration['email'].nil?
225 225 user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
226 226 user.random_password
227 227 user.register
228 228 case Setting.self_registration
229 229 when '1'
230 230 register_by_email_activation(user) do
231 231 onthefly_creation_failed(user)
232 232 end
233 233 when '3'
234 234 register_automatically(user) do
235 235 onthefly_creation_failed(user)
236 236 end
237 237 else
238 238 register_manually_by_administrator(user) do
239 239 onthefly_creation_failed(user)
240 240 end
241 241 end
242 242 else
243 243 # Existing record
244 244 if user.active?
245 245 successful_authentication(user)
246 246 else
247 247 handle_inactive_user(user)
248 248 end
249 249 end
250 250 end
251 251 end
252 252 end
253 253
254 254 def successful_authentication(user)
255 255 logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
256 256 # Valid user
257 257 self.logged_user = user
258 258 # generate a key and set cookie if autologin
259 259 if params[:autologin] && Setting.autologin?
260 260 set_autologin_cookie(user)
261 261 end
262 262 call_hook(:controller_account_success_authentication_after, {:user => user })
263 263 redirect_back_or_default my_page_path
264 264 end
265 265
266 266 def set_autologin_cookie(user)
267 267 token = Token.create(:user => user, :action => 'autologin')
268 268 secure = Redmine::Configuration['autologin_cookie_secure']
269 269 if secure.nil?
270 270 secure = request.ssl?
271 271 end
272 272 cookie_options = {
273 273 :value => token.value,
274 274 :expires => 1.year.from_now,
275 275 :path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'),
276 276 :secure => secure,
277 277 :httponly => true
278 278 }
279 279 cookies[autologin_cookie_name] = cookie_options
280 280 end
281 281
282 282 # Onthefly creation failed, display the registration form to fill/fix attributes
283 283 def onthefly_creation_failed(user, auth_source_options = { })
284 284 @user = user
285 285 session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
286 286 render :action => 'register'
287 287 end
288 288
289 289 def invalid_credentials
290 290 logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
291 291 flash.now[:error] = l(:notice_account_invalid_creditentials)
292 292 end
293 293
294 294 # Register a user for email activation.
295 295 #
296 296 # Pass a block for behavior when a user fails to save
297 297 def register_by_email_activation(user, &block)
298 298 token = Token.new(:user => user, :action => "register")
299 299 if user.save and token.save
300 300 Mailer.register(token).deliver
301 301 flash[:notice] = l(:notice_account_register_done, :email => ERB::Util.h(user.mail))
302 302 redirect_to signin_path
303 303 else
304 304 yield if block_given?
305 305 end
306 306 end
307 307
308 308 # Automatically register a user
309 309 #
310 310 # Pass a block for behavior when a user fails to save
311 311 def register_automatically(user, &block)
312 312 # Automatic activation
313 313 user.activate
314 314 user.last_login_on = Time.now
315 315 if user.save
316 316 self.logged_user = user
317 317 flash[:notice] = l(:notice_account_activated)
318 318 redirect_to my_account_path
319 319 else
320 320 yield if block_given?
321 321 end
322 322 end
323 323
324 324 # Manual activation by the administrator
325 325 #
326 326 # Pass a block for behavior when a user fails to save
327 327 def register_manually_by_administrator(user, &block)
328 328 if user.save
329 329 # Sends an email to the administrators
330 330 Mailer.account_activation_request(user).deliver
331 331 account_pending(user)
332 332 else
333 333 yield if block_given?
334 334 end
335 335 end
336 336
337 337 def handle_inactive_user(user, redirect_path=signin_path)
338 338 if user.registered?
339 339 account_pending(user, redirect_path)
340 340 else
341 341 account_locked(user, redirect_path)
342 342 end
343 343 end
344 344
345 345 def account_pending(user, redirect_path=signin_path)
346 346 if Setting.self_registration == '1'
347 347 flash[:error] = l(:notice_account_not_activated_yet, :url => activation_email_path)
348 348 session[:registered_user_id] = user.id
349 349 else
350 350 flash[:error] = l(:notice_account_pending)
351 351 end
352 352 redirect_to redirect_path
353 353 end
354 354
355 355 def account_locked(user, redirect_path=signin_path)
356 356 flash[:error] = l(:notice_account_locked)
357 357 redirect_to redirect_path
358 358 end
359 359 end
@@ -1,90 +1,90
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 ActivitiesController < ApplicationController
19 19 menu_item :activity
20 20 before_filter :find_optional_project
21 21 accept_rss_auth :index
22 22
23 23 def index
24 24 @days = Setting.activity_days_default.to_i
25 25
26 26 if params[:from]
27 27 begin; @date_to = params[:from].to_date + 1; rescue; end
28 28 end
29 29
30 30 @date_to ||= Date.today + 1
31 31 @date_from = @date_to - @days
32 32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
33 33 if params[:user_id].present?
34 34 @author = User.active.find(params[:user_id])
35 35 end
36 36
37 37 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
38 38 :with_subprojects => @with_subprojects,
39 39 :author => @author)
40 40 pref = User.current.pref
41 41 @activity.scope_select {|t| !params["show_#{t}"].nil?}
42 42 if @activity.scope.present?
43 43 if params[:submit].present?
44 44 pref.activity_scope = @activity.scope
45 45 pref.save
46 46 end
47 47 else
48 48 if @author.nil?
49 49 scope = pref.activity_scope & @activity.event_types
50 50 @activity.scope = scope.present? ? scope : :default
51 51 else
52 52 @activity.scope = :all
53 53 end
54 54 end
55 55
56 56 events = @activity.events(@date_from, @date_to)
57 57
58 58 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
59 59 respond_to do |format|
60 60 format.html {
61 61 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
62 62 render :layout => false if request.xhr?
63 63 }
64 64 format.atom {
65 65 title = l(:label_activity)
66 66 if @author
67 67 title = @author.name
68 68 elsif @activity.scope.size == 1
69 69 title = l("label_#{@activity.scope.first.singularize}_plural")
70 70 end
71 71 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
72 72 }
73 73 end
74 74 end
75 75
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 private
81 81
82 82 # TODO: refactor, duplicated in projects_controller
83 83 def find_optional_project
84 84 return true unless params[:id]
85 85 @project = Project.find(params[:id])
86 86 authorize
87 87 rescue ActiveRecord::RecordNotFound
88 88 render_404
89 89 end
90 90 end
@@ -1,84 +1,84
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 AdminController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :projects, :only => :projects
21 21 menu_item :plugins, :only => :plugins
22 22 menu_item :info, :only => :info
23 23
24 24 before_filter :require_admin
25 25 helper :sort
26 26 include SortHelper
27 27
28 28 def index
29 29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 30 end
31 31
32 32 def projects
33 33 @status = params[:status] || 1
34 34
35 35 scope = Project.status(@status).sorted
36 36 scope = scope.like(params[:name]) if params[:name].present?
37 37 @projects = scope.to_a
38 38
39 39 render :action => "projects", :layout => false if request.xhr?
40 40 end
41 41
42 42 def plugins
43 43 @plugins = Redmine::Plugin.all
44 44 end
45 45
46 46 # Loads the default configuration
47 47 # (roles, trackers, statuses, workflow, enumerations)
48 48 def default_configuration
49 49 if request.post?
50 50 begin
51 51 Redmine::DefaultData::Loader::load(params[:lang])
52 52 flash[:notice] = l(:notice_default_data_loaded)
53 53 rescue Exception => e
54 54 flash[:error] = l(:error_can_t_load_default_data, ERB::Util.h(e.message))
55 55 end
56 56 end
57 57 redirect_to admin_path
58 58 end
59 59
60 60 def test_email
61 61 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
62 62 # Force ActionMailer to raise delivery errors so we can catch it
63 63 ActionMailer::Base.raise_delivery_errors = true
64 64 begin
65 65 @test = Mailer.test_email(User.current).deliver
66 66 flash[:notice] = l(:notice_email_sent, ERB::Util.h(User.current.mail))
67 67 rescue Exception => e
68 68 flash[:error] = l(:notice_email_error, ERB::Util.h(Redmine::CodesetUtil.replace_invalid_utf8(e.message.dup)))
69 69 end
70 70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
71 71 redirect_to settings_path(:tab => 'notifications')
72 72 end
73 73
74 74 def info
75 75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
76 76 @checklist = [
77 77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
78 78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
79 79 ["#{l :text_plugin_assets_writable} (./public/plugin_assets)", File.writable?(Redmine::Plugin.public_directory)],
80 80 [:text_rmagick_available, Object.const_defined?(:Magick)],
81 81 [:text_convert_available, Redmine::Thumbnail.convert_available?]
82 82 ]
83 83 end
84 84 end
@@ -1,660 +1,660
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25 include Redmine::Pagination
26 26 include Redmine::Hook::Helper
27 27 include RoutesHelper
28 28 helper :routes
29 29
30 30 class_attribute :accept_api_auth_actions
31 31 class_attribute :accept_rss_auth_actions
32 32 class_attribute :model_object
33 33
34 34 layout 'base'
35 35
36 36 protect_from_forgery
37 37
38 38 def verify_authenticity_token
39 39 unless api_request?
40 40 super
41 41 end
42 42 end
43 43
44 44 def handle_unverified_request
45 45 unless api_request?
46 46 super
47 47 cookies.delete(autologin_cookie_name)
48 48 self.logged_user = nil
49 49 set_localization
50 50 render_error :status => 422, :message => "Invalid form authenticity token."
51 51 end
52 52 end
53 53
54 54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55 55
56 56 rescue_from ::Unauthorized, :with => :deny_access
57 57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
58 58
59 59 include Redmine::Search::Controller
60 60 include Redmine::MenuManager::MenuController
61 61 helper Redmine::MenuManager::MenuHelper
62 62
63 63 include Redmine::SudoMode::Controller
64 64
65 65 def session_expiration
66 66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 67 if session_expired? && !try_to_autologin
68 68 set_localization(User.active.find_by_id(session[:user_id]))
69 69 self.logged_user = nil
70 70 flash[:error] = l(:error_session_expired)
71 71 require_login
72 72 end
73 73 end
74 74 end
75 75
76 76 def session_expired?
77 77 ! User.verify_session_token(session[:user_id], session[:tk])
78 78 end
79 79
80 80 def start_user_session(user)
81 81 session[:user_id] = user.id
82 82 session[:tk] = user.generate_session_token
83 83 if user.must_change_password?
84 84 session[:pwd] = '1'
85 85 end
86 86 end
87 87
88 88 def user_setup
89 89 # Check the settings cache for each request
90 90 Setting.check_cache
91 91 # Find the current user
92 92 User.current = find_current_user
93 93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
94 94 end
95 95
96 96 # Returns the current user or nil if no user is logged in
97 97 # and starts a session if needed
98 98 def find_current_user
99 99 user = nil
100 100 unless api_request?
101 101 if session[:user_id]
102 102 # existing session
103 103 user = (User.active.find(session[:user_id]) rescue nil)
104 104 elsif autologin_user = try_to_autologin
105 105 user = autologin_user
106 106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
107 107 # RSS key authentication does not start a session
108 108 user = User.find_by_rss_key(params[:key])
109 109 end
110 110 end
111 111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
112 112 if (key = api_key_from_request)
113 113 # Use API key
114 114 user = User.find_by_api_key(key)
115 115 elsif request.authorization.to_s =~ /\ABasic /i
116 116 # HTTP Basic, either username/password or API key/random
117 117 authenticate_with_http_basic do |username, password|
118 118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
119 119 end
120 120 if user && user.must_change_password?
121 121 render_error :message => 'You must change your password', :status => 403
122 122 return
123 123 end
124 124 end
125 125 # Switch user if requested by an admin user
126 126 if user && user.admin? && (username = api_switch_user_from_request)
127 127 su = User.find_by_login(username)
128 128 if su && su.active?
129 129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
130 130 user = su
131 131 else
132 132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
133 133 end
134 134 end
135 135 end
136 136 user
137 137 end
138 138
139 139 def autologin_cookie_name
140 140 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
141 141 end
142 142
143 143 def try_to_autologin
144 144 if cookies[autologin_cookie_name] && Setting.autologin?
145 145 # auto-login feature starts a new session
146 146 user = User.try_to_autologin(cookies[autologin_cookie_name])
147 147 if user
148 148 reset_session
149 149 start_user_session(user)
150 150 end
151 151 user
152 152 end
153 153 end
154 154
155 155 # Sets the logged in user
156 156 def logged_user=(user)
157 157 reset_session
158 158 if user && user.is_a?(User)
159 159 User.current = user
160 160 start_user_session(user)
161 161 else
162 162 User.current = User.anonymous
163 163 end
164 164 end
165 165
166 166 # Logs out current user
167 167 def logout_user
168 168 if User.current.logged?
169 169 cookies.delete(autologin_cookie_name)
170 170 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
171 171 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
172 172 self.logged_user = nil
173 173 end
174 174 end
175 175
176 176 # check if login is globally required to access the application
177 177 def check_if_login_required
178 178 # no check needed if user is already logged in
179 179 return true if User.current.logged?
180 180 require_login if Setting.login_required?
181 181 end
182 182
183 183 def check_password_change
184 184 if session[:pwd]
185 185 if User.current.must_change_password?
186 186 flash[:error] = l(:error_password_expired)
187 187 redirect_to my_password_path
188 188 else
189 189 session.delete(:pwd)
190 190 end
191 191 end
192 192 end
193 193
194 194 def set_localization(user=User.current)
195 195 lang = nil
196 196 if user && user.logged?
197 197 lang = find_language(user.language)
198 198 end
199 199 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
200 200 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
201 201 if !accept_lang.blank?
202 202 accept_lang = accept_lang.downcase
203 203 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
204 204 end
205 205 end
206 206 lang ||= Setting.default_language
207 207 set_language_if_valid(lang)
208 208 end
209 209
210 210 def require_login
211 211 if !User.current.logged?
212 212 # Extract only the basic url parameters on non-GET requests
213 213 if request.get?
214 214 url = url_for(params)
215 215 else
216 216 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
217 217 end
218 218 respond_to do |format|
219 219 format.html {
220 220 if request.xhr?
221 221 head :unauthorized
222 222 else
223 223 redirect_to signin_path(:back_url => url)
224 224 end
225 225 }
226 226 format.any(:atom, :pdf, :csv) {
227 227 redirect_to signin_path(:back_url => url)
228 228 }
229 229 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
230 230 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
231 231 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
232 232 format.any { head :unauthorized }
233 233 end
234 234 return false
235 235 end
236 236 true
237 237 end
238 238
239 239 def require_admin
240 240 return unless require_login
241 241 if !User.current.admin?
242 242 render_403
243 243 return false
244 244 end
245 245 true
246 246 end
247 247
248 248 def deny_access
249 249 User.current.logged? ? render_403 : require_login
250 250 end
251 251
252 252 # Authorize the user for the requested action
253 253 def authorize(ctrl = params[:controller], action = params[:action], global = false)
254 254 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
255 255 if allowed
256 256 true
257 257 else
258 258 if @project && @project.archived?
259 259 render_403 :message => :notice_not_authorized_archived_project
260 260 else
261 261 deny_access
262 262 end
263 263 end
264 264 end
265 265
266 266 # Authorize the user for the requested action outside a project
267 267 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
268 268 authorize(ctrl, action, global)
269 269 end
270 270
271 271 # Find project of id params[:id]
272 272 def find_project
273 273 @project = Project.find(params[:id])
274 274 rescue ActiveRecord::RecordNotFound
275 275 render_404
276 276 end
277 277
278 278 # Find project of id params[:project_id]
279 279 def find_project_by_project_id
280 280 @project = Project.find(params[:project_id])
281 281 rescue ActiveRecord::RecordNotFound
282 282 render_404
283 283 end
284 284
285 285 # Find a project based on params[:project_id]
286 286 # TODO: some subclasses override this, see about merging their logic
287 287 def find_optional_project
288 288 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
289 289 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
290 290 allowed ? true : deny_access
291 291 rescue ActiveRecord::RecordNotFound
292 292 render_404
293 293 end
294 294
295 295 # Finds and sets @project based on @object.project
296 296 def find_project_from_association
297 297 render_404 unless @object.present?
298 298
299 299 @project = @object.project
300 300 end
301 301
302 302 def find_model_object
303 303 model = self.class.model_object
304 304 if model
305 305 @object = model.find(params[:id])
306 306 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
307 307 end
308 308 rescue ActiveRecord::RecordNotFound
309 309 render_404
310 310 end
311 311
312 312 def self.model_object(model)
313 313 self.model_object = model
314 314 end
315 315
316 316 # Find the issue whose id is the :id parameter
317 317 # Raises a Unauthorized exception if the issue is not visible
318 318 def find_issue
319 319 # Issue.visible.find(...) can not be used to redirect user to the login form
320 320 # if the issue actually exists but requires authentication
321 321 @issue = Issue.find(params[:id])
322 322 raise Unauthorized unless @issue.visible?
323 323 @project = @issue.project
324 324 rescue ActiveRecord::RecordNotFound
325 325 render_404
326 326 end
327 327
328 328 # Find issues with a single :id param or :ids array param
329 329 # Raises a Unauthorized exception if one of the issues is not visible
330 330 def find_issues
331 331 @issues = Issue.
332 332 where(:id => (params[:id] || params[:ids])).
333 333 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
334 334 to_a
335 335 raise ActiveRecord::RecordNotFound if @issues.empty?
336 336 raise Unauthorized unless @issues.all?(&:visible?)
337 337 @projects = @issues.collect(&:project).compact.uniq
338 338 @project = @projects.first if @projects.size == 1
339 339 rescue ActiveRecord::RecordNotFound
340 340 render_404
341 341 end
342 342
343 343 def find_attachments
344 344 if (attachments = params[:attachments]).present?
345 345 att = attachments.values.collect do |attachment|
346 346 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
347 347 end
348 348 att.compact!
349 349 end
350 350 @attachments = att || []
351 351 end
352 352
353 353 # make sure that the user is a member of the project (or admin) if project is private
354 354 # used as a before_filter for actions that do not require any particular permission on the project
355 355 def check_project_privacy
356 356 if @project && !@project.archived?
357 357 if @project.visible?
358 358 true
359 359 else
360 360 deny_access
361 361 end
362 362 else
363 363 @project = nil
364 364 render_404
365 365 false
366 366 end
367 367 end
368 368
369 369 def back_url
370 370 url = params[:back_url]
371 371 if url.nil? && referer = request.env['HTTP_REFERER']
372 372 url = CGI.unescape(referer.to_s)
373 373 end
374 374 url
375 375 end
376 376
377 377 def redirect_back_or_default(default, options={})
378 378 back_url = params[:back_url].to_s
379 379 if back_url.present? && valid_url = validate_back_url(back_url)
380 380 redirect_to(valid_url)
381 381 return
382 382 elsif options[:referer]
383 383 redirect_to_referer_or default
384 384 return
385 385 end
386 386 redirect_to default
387 387 false
388 388 end
389 389
390 390 # Returns a validated URL string if back_url is a valid url for redirection,
391 391 # otherwise false
392 392 def validate_back_url(back_url)
393 393 if CGI.unescape(back_url).include?('..')
394 394 return false
395 395 end
396 396
397 397 begin
398 398 uri = URI.parse(back_url)
399 399 rescue URI::InvalidURIError
400 400 return false
401 401 end
402 402
403 403 [:scheme, :host, :port].each do |component|
404 404 if uri.send(component).present? && uri.send(component) != request.send(component)
405 405 return false
406 406 end
407 407 uri.send(:"#{component}=", nil)
408 408 end
409 409 # Always ignore basic user:password in the URL
410 410 uri.userinfo = nil
411 411
412 412 path = uri.to_s
413 413 # Ensure that the remaining URL starts with a slash, followed by a
414 414 # non-slash character or the end
415 415 if path !~ %r{\A/([^/]|\z)}
416 416 return false
417 417 end
418 418
419 419 if path.match(%r{/(login|account/register)})
420 420 return false
421 421 end
422 422
423 423 if relative_url_root.present? && !path.starts_with?(relative_url_root)
424 424 return false
425 425 end
426 426
427 427 return path
428 428 end
429 429 private :validate_back_url
430 430
431 431 def valid_back_url?(back_url)
432 432 !!validate_back_url(back_url)
433 433 end
434 434 private :valid_back_url?
435 435
436 436 # Redirects to the request referer if present, redirects to args or call block otherwise.
437 437 def redirect_to_referer_or(*args, &block)
438 438 redirect_to :back
439 439 rescue ::ActionController::RedirectBackError
440 440 if args.any?
441 441 redirect_to *args
442 442 elsif block_given?
443 443 block.call
444 444 else
445 445 raise "#redirect_to_referer_or takes arguments or a block"
446 446 end
447 447 end
448 448
449 449 def render_403(options={})
450 450 @project = nil
451 451 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
452 452 return false
453 453 end
454 454
455 455 def render_404(options={})
456 456 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
457 457 return false
458 458 end
459 459
460 460 # Renders an error response
461 461 def render_error(arg)
462 462 arg = {:message => arg} unless arg.is_a?(Hash)
463 463
464 464 @message = arg[:message]
465 465 @message = l(@message) if @message.is_a?(Symbol)
466 466 @status = arg[:status] || 500
467 467
468 468 respond_to do |format|
469 469 format.html {
470 470 render :template => 'common/error', :layout => use_layout, :status => @status
471 471 }
472 472 format.any { head @status }
473 473 end
474 474 end
475 475
476 476 # Handler for ActionView::MissingTemplate exception
477 477 def missing_template
478 478 logger.warn "Missing template, responding with 404"
479 479 @project = nil
480 480 render_404
481 481 end
482 482
483 483 # Filter for actions that provide an API response
484 484 # but have no HTML representation for non admin users
485 485 def require_admin_or_api_request
486 486 return true if api_request?
487 487 if User.current.admin?
488 488 true
489 489 elsif User.current.logged?
490 490 render_error(:status => 406)
491 491 else
492 492 deny_access
493 493 end
494 494 end
495 495
496 496 # Picks which layout to use based on the request
497 497 #
498 498 # @return [boolean, string] name of the layout to use or false for no layout
499 499 def use_layout
500 500 request.xhr? ? false : 'base'
501 501 end
502 502
503 503 def render_feed(items, options={})
504 504 @items = (items || []).to_a
505 505 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
506 506 @items = @items.slice(0, Setting.feeds_limit.to_i)
507 507 @title = options[:title] || Setting.app_title
508 508 render :template => "common/feed", :formats => [:atom], :layout => false,
509 509 :content_type => 'application/atom+xml'
510 510 end
511 511
512 512 def self.accept_rss_auth(*actions)
513 513 if actions.any?
514 514 self.accept_rss_auth_actions = actions
515 515 else
516 516 self.accept_rss_auth_actions || []
517 517 end
518 518 end
519 519
520 520 def accept_rss_auth?(action=action_name)
521 521 self.class.accept_rss_auth.include?(action.to_sym)
522 522 end
523 523
524 524 def self.accept_api_auth(*actions)
525 525 if actions.any?
526 526 self.accept_api_auth_actions = actions
527 527 else
528 528 self.accept_api_auth_actions || []
529 529 end
530 530 end
531 531
532 532 def accept_api_auth?(action=action_name)
533 533 self.class.accept_api_auth.include?(action.to_sym)
534 534 end
535 535
536 536 # Returns the number of objects that should be displayed
537 537 # on the paginated list
538 538 def per_page_option
539 539 per_page = nil
540 540 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
541 541 per_page = params[:per_page].to_s.to_i
542 542 session[:per_page] = per_page
543 543 elsif session[:per_page]
544 544 per_page = session[:per_page]
545 545 else
546 546 per_page = Setting.per_page_options_array.first || 25
547 547 end
548 548 per_page
549 549 end
550 550
551 551 # Returns offset and limit used to retrieve objects
552 552 # for an API response based on offset, limit and page parameters
553 553 def api_offset_and_limit(options=params)
554 554 if options[:offset].present?
555 555 offset = options[:offset].to_i
556 556 if offset < 0
557 557 offset = 0
558 558 end
559 559 end
560 560 limit = options[:limit].to_i
561 561 if limit < 1
562 562 limit = 25
563 563 elsif limit > 100
564 564 limit = 100
565 565 end
566 566 if offset.nil? && options[:page].present?
567 567 offset = (options[:page].to_i - 1) * limit
568 568 offset = 0 if offset < 0
569 569 end
570 570 offset ||= 0
571 571
572 572 [offset, limit]
573 573 end
574 574
575 575 # qvalues http header parser
576 576 # code taken from webrick
577 577 def parse_qvalues(value)
578 578 tmp = []
579 579 if value
580 580 parts = value.split(/,\s*/)
581 581 parts.each {|part|
582 582 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
583 583 val = m[1]
584 584 q = (m[2] or 1).to_f
585 585 tmp.push([val, q])
586 586 end
587 587 }
588 588 tmp = tmp.sort_by{|val, q| -q}
589 589 tmp.collect!{|val, q| val}
590 590 end
591 591 return tmp
592 592 rescue
593 593 nil
594 594 end
595 595
596 596 # Returns a string that can be used as filename value in Content-Disposition header
597 597 def filename_for_content_disposition(name)
598 598 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident|Edge)} ? ERB::Util.url_encode(name) : name
599 599 end
600 600
601 601 def api_request?
602 602 %w(xml json).include? params[:format]
603 603 end
604 604
605 605 # Returns the API key present in the request
606 606 def api_key_from_request
607 607 if params[:key].present?
608 608 params[:key].to_s
609 609 elsif request.headers["X-Redmine-API-Key"].present?
610 610 request.headers["X-Redmine-API-Key"].to_s
611 611 end
612 612 end
613 613
614 614 # Returns the API 'switch user' value if present
615 615 def api_switch_user_from_request
616 616 request.headers["X-Redmine-Switch-User"].to_s.presence
617 617 end
618 618
619 619 # Renders a warning flash if obj has unsaved attachments
620 620 def render_attachment_warning_if_needed(obj)
621 621 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
622 622 end
623 623
624 624 # Rescues an invalid query statement. Just in case...
625 625 def query_statement_invalid(exception)
626 626 logger.error "Query::StatementInvalid: #{exception.message}" if logger
627 627 session.delete(:query)
628 628 sort_clear if respond_to?(:sort_clear)
629 629 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
630 630 end
631 631
632 632 # Renders a 200 response for successfull updates or deletions via the API
633 633 def render_api_ok
634 634 render_api_head :ok
635 635 end
636 636
637 637 # Renders a head API response
638 638 def render_api_head(status)
639 639 # #head would return a response body with one space
640 640 render :text => '', :status => status, :layout => nil
641 641 end
642 642
643 643 # Renders API response on validation failure
644 644 # for an object or an array of objects
645 645 def render_validation_errors(objects)
646 646 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
647 647 render_api_errors(messages)
648 648 end
649 649
650 650 def render_api_errors(*messages)
651 651 @error_messages = messages.flatten
652 652 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
653 653 end
654 654
655 655 # Overrides #_include_layout? so that #render with no arguments
656 656 # doesn't use the layout for api requests
657 657 def _include_layout?(*args)
658 658 api_request? ? false : super
659 659 end
660 660 end
@@ -1,191 +1,191
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
20 20 before_filter :find_editable_attachments, :only => [:edit, :update]
21 21 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
22 22 before_filter :delete_authorize, :only => :destroy
23 23 before_filter :authorize_global, :only => :upload
24 24
25 25 accept_api_auth :show, :download, :thumbnail, :upload
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.new(@attachment.diskfile, "rb").read
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.new(@attachment.diskfile, "rb").read
42 42 render :action => 'file'
43 43 else
44 44 download
45 45 end
46 46 }
47 47 format.api
48 48 end
49 49 end
50 50
51 51 def download
52 52 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
53 53 @attachment.increment_download
54 54 end
55 55
56 56 if stale?(:etag => @attachment.digest)
57 57 # images are sent inline
58 58 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
59 59 :type => detect_content_type(@attachment),
60 60 :disposition => (@attachment.image? ? 'inline' : 'attachment')
61 61 end
62 62 end
63 63
64 64 def thumbnail
65 65 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
66 66 if stale?(:etag => tbnail)
67 67 send_file tbnail,
68 68 :filename => filename_for_content_disposition(@attachment.filename),
69 69 :type => detect_content_type(@attachment),
70 70 :disposition => 'inline'
71 71 end
72 72 else
73 73 # No thumbnail for the attachment or thumbnail could not be created
74 74 render :nothing => true, :status => 404
75 75 end
76 76 end
77 77
78 78 def upload
79 79 # Make sure that API users get used to set this content type
80 80 # as it won't trigger Rails' automatic parsing of the request body for parameters
81 81 unless request.content_type == 'application/octet-stream'
82 82 render :nothing => true, :status => 406
83 83 return
84 84 end
85 85
86 86 @attachment = Attachment.new(:file => request.raw_post)
87 87 @attachment.author = User.current
88 88 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
89 89 @attachment.content_type = params[:content_type].presence
90 90 saved = @attachment.save
91 91
92 92 respond_to do |format|
93 93 format.js
94 94 format.api {
95 95 if saved
96 96 render :action => 'upload', :status => :created
97 97 else
98 98 render_validation_errors(@attachment)
99 99 end
100 100 }
101 101 end
102 102 end
103 103
104 104 def edit
105 105 end
106 106
107 107 def update
108 108 if params[:attachments].is_a?(Hash)
109 109 if Attachment.update_attachments(@attachments, params[:attachments])
110 110 redirect_back_or_default home_path
111 111 return
112 112 end
113 113 end
114 114 render :action => 'edit'
115 115 end
116 116
117 117 def destroy
118 118 if @attachment.container.respond_to?(:init_journal)
119 119 @attachment.container.init_journal(User.current)
120 120 end
121 121 if @attachment.container
122 122 # Make sure association callbacks are called
123 123 @attachment.container.attachments.delete(@attachment)
124 124 else
125 125 @attachment.destroy
126 126 end
127 127
128 128 respond_to do |format|
129 129 format.html { redirect_to_referer_or project_path(@project) }
130 130 format.js
131 131 end
132 132 end
133 133
134 134 private
135 135
136 136 def find_attachment
137 137 @attachment = Attachment.find(params[:id])
138 138 # Show 404 if the filename in the url is wrong
139 139 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
140 140 @project = @attachment.project
141 141 rescue ActiveRecord::RecordNotFound
142 142 render_404
143 143 end
144 144
145 145 def find_editable_attachments
146 146 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
147 147 unless klass && klass.reflect_on_association(:attachments)
148 148 render_404
149 149 return
150 150 end
151 151
152 152 @container = klass.find(params[:object_id])
153 153 if @container.respond_to?(:visible?) && !@container.visible?
154 154 render_403
155 155 return
156 156 end
157 157 @attachments = @container.attachments.select(&:editable?)
158 158 if @container.respond_to?(:project)
159 159 @project = @container.project
160 160 end
161 161 render_404 if @attachments.empty?
162 162 rescue ActiveRecord::RecordNotFound
163 163 render_404
164 164 end
165 165
166 166 # Checks that the file exists and is readable
167 167 def file_readable
168 168 if @attachment.readable?
169 169 true
170 170 else
171 171 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
172 172 render_404
173 173 end
174 174 end
175 175
176 176 def read_authorize
177 177 @attachment.visible? ? true : deny_access
178 178 end
179 179
180 180 def delete_authorize
181 181 @attachment.deletable? ? true : deny_access
182 182 end
183 183
184 184 def detect_content_type(attachment)
185 185 content_type = attachment.content_type
186 186 if content_type.blank? || content_type == "application/octet-stream"
187 187 content_type = Redmine::MimeType.of(attachment.filename)
188 188 end
189 189 content_type.to_s
190 190 end
191 191 end
@@ -1,97 +1,97
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 AuthSourcesController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :ldap_authentication
21 21
22 22 before_filter :require_admin
23 23 before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
24 24 require_sudo_mode :update, :destroy
25 25
26 26 def index
27 27 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
28 28 end
29 29
30 30 def new
31 31 klass_name = params[:type] || 'AuthSourceLdap'
32 32 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
33 33 render_404 unless @auth_source
34 34 end
35 35
36 36 def create
37 37 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
38 38 if @auth_source.save
39 39 flash[:notice] = l(:notice_successful_create)
40 40 redirect_to auth_sources_path
41 41 else
42 42 render :action => 'new'
43 43 end
44 44 end
45 45
46 46 def edit
47 47 end
48 48
49 49 def update
50 50 if @auth_source.update_attributes(params[:auth_source])
51 51 flash[:notice] = l(:notice_successful_update)
52 52 redirect_to auth_sources_path
53 53 else
54 54 render :action => 'edit'
55 55 end
56 56 end
57 57
58 58 def test_connection
59 59 begin
60 60 @auth_source.test_connection
61 61 flash[:notice] = l(:notice_successful_connection)
62 62 rescue Exception => e
63 63 flash[:error] = l(:error_unable_to_connect, e.message)
64 64 end
65 65 redirect_to auth_sources_path
66 66 end
67 67
68 68 def destroy
69 69 unless @auth_source.users.exists?
70 70 @auth_source.destroy
71 71 flash[:notice] = l(:notice_successful_delete)
72 72 end
73 73 redirect_to auth_sources_path
74 74 end
75 75
76 76 def autocomplete_for_new_user
77 77 results = AuthSource.search(params[:term])
78 78
79 79 render :json => results.map {|result| {
80 80 'value' => result[:login],
81 81 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})",
82 82 'login' => result[:login].to_s,
83 83 'firstname' => result[:firstname].to_s,
84 84 'lastname' => result[:lastname].to_s,
85 85 'mail' => result[:mail].to_s,
86 86 'auth_source_id' => result[:auth_source_id].to_s
87 87 }}
88 88 end
89 89
90 90 private
91 91
92 92 def find_auth_source
93 93 @auth_source = AuthSource.find(params[:id])
94 94 rescue ActiveRecord::RecordNotFound
95 95 render_404
96 96 end
97 97 end
@@ -1,44 +1,44
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 AutoCompletesController < ApplicationController
19 19 before_filter :find_project
20 20
21 21 def issues
22 22 @issues = []
23 23 q = (params[:q] || params[:term]).to_s.strip
24 24 if q.present?
25 25 scope = Issue.cross_project_scope(@project, params[:scope]).visible
26 26 if q.match(/\A#?(\d+)\z/)
27 27 @issues << scope.find_by_id($1.to_i)
28 28 end
29 29 @issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE LOWER(?)", "%#{q}%").order("#{Issue.table_name}.id DESC").limit(10).to_a
30 30 @issues.compact!
31 31 end
32 32 render :layout => false
33 33 end
34 34
35 35 private
36 36
37 37 def find_project
38 38 if params[:project_id].present?
39 39 @project = Project.find(params[:project_id])
40 40 end
41 41 rescue ActiveRecord::RecordNotFound
42 42 render_404
43 43 end
44 44 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :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}.created_on",
41 41 'replies' => "#{Message.table_name}.replies_count",
42 42 'updated_on' => "COALESCE(last_replies_messages.created_on, #{Message.table_name}.created_on)"
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("#{Message.table_name}.sticky DESC").
48 48 joins("LEFT OUTER JOIN #{Message.table_name} last_replies_messages ON last_replies_messages.id = #{Message.table_name}.last_reply_id").
49 49 limit(@topic_pages.per_page).
50 50 offset(@topic_pages.offset).
51 51 order(sort_clause).
52 52 preload(:author, {:last_reply => :author}).
53 53 to_a
54 54 @message = Message.new(:board => @board)
55 55 render :action => 'show', :layout => !request.xhr?
56 56 }
57 57 format.atom {
58 58 @messages = @board.messages.
59 59 reorder('created_on DESC').
60 60 includes(:author, :board).
61 61 limit(Setting.feeds_limit.to_i).
62 62 to_a
63 63 render_feed(@messages, :title => "#{@project}: #{@board}")
64 64 }
65 65 end
66 66 end
67 67
68 68 def new
69 69 @board = @project.boards.build
70 70 @board.safe_attributes = params[:board]
71 71 end
72 72
73 73 def create
74 74 @board = @project.boards.build
75 75 @board.safe_attributes = params[:board]
76 76 if @board.save
77 77 flash[:notice] = l(:notice_successful_create)
78 78 redirect_to_settings_in_projects
79 79 else
80 80 render :action => 'new'
81 81 end
82 82 end
83 83
84 84 def edit
85 85 end
86 86
87 87 def update
88 88 @board.safe_attributes = params[:board]
89 89 if @board.save
90 90 redirect_to_settings_in_projects
91 91 else
92 92 render :action => 'edit'
93 93 end
94 94 end
95 95
96 96 def destroy
97 97 @board.destroy
98 98 redirect_to_settings_in_projects
99 99 end
100 100
101 101 private
102 102 def redirect_to_settings_in_projects
103 103 redirect_to settings_project_path(@project, :tab => 'boards')
104 104 end
105 105
106 106 def find_board_if_available
107 107 @board = @project.boards.find(params[:id]) if params[:id]
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CalendarsController < ApplicationController
19 19 menu_item :calendar
20 20 before_filter :find_optional_project
21 21
22 22 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
23 23
24 24 helper :issues
25 25 helper :projects
26 26 helper :queries
27 27 include QueriesHelper
28 28 helper :sort
29 29 include SortHelper
30 30
31 31 def show
32 32 if params[:year] and params[:year].to_i > 1900
33 33 @year = params[:year].to_i
34 34 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
35 35 @month = params[:month].to_i
36 36 end
37 37 end
38 38 @year ||= Date.today.year
39 39 @month ||= Date.today.month
40 40
41 41 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
42 42 retrieve_query
43 43 @query.group_by = nil
44 44 if @query.valid?
45 45 events = []
46 46 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
47 47 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
48 48 )
49 49 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
50 50
51 51 @calendar.events = events
52 52 end
53 53
54 54 render :action => 'show', :layout => false if request.xhr?
55 55 end
56 56 end
@@ -1,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CommentsController < ApplicationController
19 19 default_search_scope :news
20 20 model_object News
21 21 before_filter :find_model_object
22 22 before_filter :find_project_from_association
23 23 before_filter :authorize
24 24
25 25 def create
26 26 raise Unauthorized unless @news.commentable?
27 27
28 28 @comment = Comment.new
29 29 @comment.safe_attributes = params[:comment]
30 30 @comment.author = User.current
31 31 if @news.comments << @comment
32 32 flash[:notice] = l(:label_comment_added)
33 33 end
34 34
35 35 redirect_to news_path(@news)
36 36 end
37 37
38 38 def destroy
39 39 @news.comments.find(params[:comment_id]).destroy
40 40 redirect_to news_path(@news)
41 41 end
42 42
43 43 private
44 44
45 45 # ApplicationController's find_model_object sets it based on the controller
46 46 # name so it needs to be overriden and set to @news instead
47 47 def find_model_object
48 48 super
49 49 @news = @object
50 50 @comment = nil
51 51 @news
52 52 end
53 53 end
@@ -1,97 +1,97
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 ContextMenusController < ApplicationController
19 19 helper :watchers
20 20 helper :issues
21 21
22 22 before_filter :find_issues, :only => :issues
23 23
24 24 def issues
25 25 if (@issues.size == 1)
26 26 @issue = @issues.first
27 27 end
28 28 @issue_ids = @issues.map(&:id).sort
29 29
30 30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31 31
32 32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
33 33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
35 35 :delete => User.current.allowed_to?(:delete_issues, @projects)
36 36 }
37 37 if @project
38 38 if @issue
39 39 @assignables = @issue.assignable_users
40 40 else
41 41 @assignables = @project.assignable_users
42 42 end
43 43 @trackers = @project.trackers
44 44 else
45 45 #when multiple projects, we only keep the intersection of each set
46 46 @assignables = @projects.map(&:assignable_users).reduce(:&)
47 47 @trackers = @projects.map(&:trackers).reduce(:&)
48 48 end
49 49 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
50 50
51 51 @priorities = IssuePriority.active.reverse
52 52 @back = back_url
53 53
54 54 @options_by_custom_field = {}
55 55 if @can[:edit]
56 56 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
57 57 custom_fields.each do |field|
58 58 values = field.possible_values_options(@projects)
59 59 if values.present?
60 60 @options_by_custom_field[field] = values
61 61 end
62 62 end
63 63 end
64 64
65 65 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
66 66 render :layout => false
67 67 end
68 68
69 69 def time_entries
70 70 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
71 71 (render_404; return) unless @time_entries.present?
72 72 if (@time_entries.size == 1)
73 73 @time_entry = @time_entries.first
74 74 end
75 75
76 76 @projects = @time_entries.collect(&:project).compact.uniq
77 77 @project = @projects.first if @projects.size == 1
78 78 @activities = TimeEntryActivity.shared.active
79 79
80 80 edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
81 81 @can = {:edit => edit_allowed, :delete => edit_allowed}
82 82 @back = back_url
83 83
84 84 @options_by_custom_field = {}
85 85 if @can[:edit]
86 86 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
87 87 custom_fields.each do |field|
88 88 values = field.possible_values_options(@projects)
89 89 if values.present?
90 90 @options_by_custom_field[field] = values
91 91 end
92 92 end
93 93 end
94 94
95 95 render :layout => false
96 96 end
97 97 end
@@ -1,71 +1,71
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CustomFieldEnumerationsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_custom_field
23 23 before_filter :find_enumeration, :only => :destroy
24 24
25 25 helper :custom_fields
26 26
27 27 def index
28 28 @values = @custom_field.enumerations.order(:position)
29 29 end
30 30
31 31 def create
32 32 @value = @custom_field.enumerations.build(params[:custom_field_enumeration])
33 33 @value.save
34 34 respond_to do |format|
35 35 format.html { redirect_to custom_field_enumerations_path(@custom_field) }
36 36 format.js
37 37 end
38 38 end
39 39
40 40 def update_each
41 41 if CustomFieldEnumeration.update_each(@custom_field, params[:custom_field_enumerations])
42 42 flash[:notice] = l(:notice_successful_update)
43 43 end
44 44 redirect_to :action => 'index'
45 45 end
46 46
47 47 def destroy
48 48 reassign_to = @custom_field.enumerations.find_by_id(params[:reassign_to_id])
49 49 if reassign_to.nil? && @value.in_use?
50 50 @enumerations = @custom_field.enumerations - [@value]
51 51 render :action => 'destroy'
52 52 return
53 53 end
54 54 @value.destroy(reassign_to)
55 55 redirect_to custom_field_enumerations_path(@custom_field)
56 56 end
57 57
58 58 private
59 59
60 60 def find_custom_field
61 61 @custom_field = CustomField.find(params[:custom_field_id])
62 62 rescue ActiveRecord::RecordNotFound
63 63 render_404
64 64 end
65 65
66 66 def find_enumeration
67 67 @value = @custom_field.enumerations.find(params[:id])
68 68 rescue ActiveRecord::RecordNotFound
69 69 render_404
70 70 end
71 71 end
@@ -1,88 +1,88
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_admin
22 22 before_filter :build_new_custom_field, :only => [:new, :create]
23 23 before_filter :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 flash[:notice] = l(:notice_successful_update)
58 58 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
59 59 redirect_back_or_default edit_custom_field_path(@custom_field)
60 60 else
61 61 render :action => 'edit'
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 begin
67 67 @custom_field.destroy
68 68 rescue
69 69 flash[:error] = l(:error_can_not_delete_custom_field)
70 70 end
71 71 redirect_to custom_fields_path(:tab => @custom_field.class.name)
72 72 end
73 73
74 74 private
75 75
76 76 def build_new_custom_field
77 77 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
78 78 if @custom_field.nil?
79 79 render :action => 'select_type'
80 80 end
81 81 end
82 82
83 83 def find_custom_field
84 84 @custom_field = CustomField.find(params[:id])
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 DocumentsController < ApplicationController
19 19 default_search_scope :documents
20 20 model_object Document
21 21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
22 22 before_filter :find_model_object, :except => [:index, :new, :create]
23 23 before_filter :find_project_from_association, :except => [:index, :new, :create]
24 24 before_filter :authorize
25 25
26 26 helper :attachments
27 27 helper :custom_fields
28 28
29 29 def index
30 30 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
31 31 documents = @project.documents.includes(:attachments, :category).to_a
32 32 case @sort_by
33 33 when 'date'
34 34 @grouped = documents.group_by {|d| d.updated_on.to_date }
35 35 when 'title'
36 36 @grouped = documents.group_by {|d| d.title.first.upcase}
37 37 when 'author'
38 38 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
39 39 else
40 40 @grouped = documents.group_by(&:category)
41 41 end
42 42 @document = @project.documents.build
43 43 render :layout => false if request.xhr?
44 44 end
45 45
46 46 def show
47 47 @attachments = @document.attachments.to_a
48 48 end
49 49
50 50 def new
51 51 @document = @project.documents.build
52 52 @document.safe_attributes = params[:document]
53 53 end
54 54
55 55 def create
56 56 @document = @project.documents.build
57 57 @document.safe_attributes = params[:document]
58 58 @document.save_attachments(params[:attachments])
59 59 if @document.save
60 60 render_attachment_warning_if_needed(@document)
61 61 flash[:notice] = l(:notice_successful_create)
62 62 redirect_to project_documents_path(@project)
63 63 else
64 64 render :action => 'new'
65 65 end
66 66 end
67 67
68 68 def edit
69 69 end
70 70
71 71 def update
72 72 @document.safe_attributes = params[:document]
73 73 if @document.save
74 74 flash[:notice] = l(:notice_successful_update)
75 75 redirect_to document_path(@document)
76 76 else
77 77 render :action => 'edit'
78 78 end
79 79 end
80 80
81 81 def destroy
82 82 @document.destroy if request.delete?
83 83 redirect_to project_documents_path(@project)
84 84 end
85 85
86 86 def add_attachment
87 87 attachments = Attachment.attach_files(@document, params[:attachments])
88 88 render_attachment_warning_if_needed(@document)
89 89
90 90 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
91 91 Mailer.attachments_added(attachments[:files]).deliver
92 92 end
93 93 redirect_to document_path(@document)
94 94 end
95 95 end
@@ -1,106 +1,106
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 EmailAddressesController < ApplicationController
19 19 before_filter :find_user, :require_admin_or_current_user
20 20 before_filter :find_email_address, :only => [:update, :destroy]
21 21 require_sudo_mode :create, :update, :destroy
22 22
23 23 def index
24 24 @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a
25 25 @address ||= EmailAddress.new
26 26 end
27 27
28 28 def create
29 29 saved = false
30 30 if @user.email_addresses.count <= Setting.max_additional_emails.to_i
31 31 @address = EmailAddress.new(:user => @user, :is_default => false)
32 32 attrs = params[:email_address]
33 33 if attrs.is_a?(Hash)
34 34 @address.address = attrs[:address].to_s
35 35 end
36 36 saved = @address.save
37 37 end
38 38
39 39 respond_to do |format|
40 40 format.html {
41 41 if saved
42 42 redirect_to user_email_addresses_path(@user)
43 43 else
44 44 index
45 45 render :action => 'index'
46 46 end
47 47 }
48 48 format.js {
49 49 @address = nil if saved
50 50 index
51 51 render :action => 'index'
52 52 }
53 53 end
54 54 end
55 55
56 56 def update
57 57 if params[:notify].present?
58 58 @address.notify = params[:notify].to_s
59 59 end
60 60 @address.save
61 61
62 62 respond_to do |format|
63 63 format.html {
64 64 redirect_to user_email_addresses_path(@user)
65 65 }
66 66 format.js {
67 67 @address = nil
68 68 index
69 69 render :action => 'index'
70 70 }
71 71 end
72 72 end
73 73
74 74 def destroy
75 75 @address.destroy
76 76
77 77 respond_to do |format|
78 78 format.html {
79 79 redirect_to user_email_addresses_path(@user)
80 80 }
81 81 format.js {
82 82 @address = nil
83 83 index
84 84 render :action => 'index'
85 85 }
86 86 end
87 87 end
88 88
89 89 private
90 90
91 91 def find_user
92 92 @user = User.find(params[:user_id])
93 93 end
94 94
95 95 def find_email_address
96 96 @address = @user.email_addresses.where(:is_default => false).find(params[:id])
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100
101 101 def require_admin_or_current_user
102 102 unless @user == User.current
103 103 require_admin
104 104 end
105 105 end
106 106 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 before_filter :build_new_enumeration, :only => [:new, :create]
24 24 before_filter :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 flash[:notice] = l(:notice_successful_update)
61 61 redirect_to enumerations_path
62 62 else
63 63 render :action => 'edit'
64 64 end
65 65 end
66 66
67 67 def destroy
68 68 if !@enumeration.in_use?
69 69 # No associated objects
70 70 @enumeration.destroy
71 71 redirect_to enumerations_path
72 72 return
73 73 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
74 74 @enumeration.destroy(reassign_to)
75 75 redirect_to enumerations_path
76 76 return
77 77 end
78 78 @enumerations = @enumeration.class.system.to_a - [@enumeration]
79 79 end
80 80
81 81 private
82 82
83 83 def build_new_enumeration
84 84 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
85 85 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
86 86 if @enumeration.nil?
87 87 render_404
88 88 end
89 89 end
90 90
91 91 def find_enumeration
92 92 @enumeration = Enumeration.find(params[:id])
93 93 rescue ActiveRecord::RecordNotFound
94 94 render_404
95 95 end
96 96 end
@@ -1,62 +1,62
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 FilesController < ApplicationController
19 19 menu_item :files
20 20
21 21 before_filter :find_project_by_project_id
22 22 before_filter :authorize
23 23
24 24 helper :sort
25 25 include SortHelper
26 26
27 27 def index
28 28 sort_init 'filename', 'asc'
29 29 sort_update 'filename' => "#{Attachment.table_name}.filename",
30 30 'created_on' => "#{Attachment.table_name}.created_on",
31 31 'size' => "#{Attachment.table_name}.filesize",
32 32 'downloads' => "#{Attachment.table_name}.downloads"
33 33
34 34 @containers = [Project.includes(:attachments).
35 35 references(:attachments).reorder(sort_clause).find(@project.id)]
36 36 @containers += @project.versions.includes(:attachments).
37 37 references(:attachments).reorder(sort_clause).to_a.sort.reverse
38 38 render :layout => !request.xhr?
39 39 end
40 40
41 41 def new
42 42 @versions = @project.versions.sort
43 43 end
44 44
45 45 def create
46 46 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
47 47 attachments = Attachment.attach_files(container, params[:attachments])
48 48 render_attachment_warning_if_needed(container)
49 49
50 50 if attachments[:files].present?
51 51 if Setting.notified_events.include?('file_added')
52 52 Mailer.attachments_added(attachments[:files]).deliver
53 53 end
54 54 flash[:notice] = l(:label_file_added)
55 55 redirect_to project_files_path(@project)
56 56 else
57 57 flash.now[:error] = l(:label_attachment) + " " + l('activerecord.errors.messages.invalid')
58 58 new
59 59 render :action => 'new'
60 60 end
61 61 end
62 62 end
@@ -1,48 +1,48
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 GanttsController < ApplicationController
19 19 menu_item :gantt
20 20 before_filter :find_optional_project
21 21
22 22 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
23 23
24 24 helper :gantt
25 25 helper :issues
26 26 helper :projects
27 27 helper :queries
28 28 include QueriesHelper
29 29 helper :sort
30 30 include SortHelper
31 31 include Redmine::Export::PDF
32 32
33 33 def show
34 34 @gantt = Redmine::Helpers::Gantt.new(params)
35 35 @gantt.project = @project
36 36 retrieve_query
37 37 @query.group_by = nil
38 38 @gantt.query = @query if @query.valid?
39 39
40 40 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
41 41
42 42 respond_to do |format|
43 43 format.html { render :action => "show", :layout => !request.xhr? }
44 44 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
45 45 format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
46 46 end
47 47 end
48 48 end
@@ -1,149 +1,149
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 GroupsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_group, :except => [:index, :new, :create]
23 23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24 24
25 25 require_sudo_mode :add_users, :remove_user, :create, :update, :destroy, :edit_membership, :destroy_membership
26 26
27 27 helper :custom_fields
28 28 helper :principal_memberships
29 29
30 30 def index
31 31 respond_to do |format|
32 32 format.html {
33 33 @groups = Group.sorted.to_a
34 34 @user_count_by_group_id = user_count_by_group_id
35 35 }
36 36 format.api {
37 37 scope = Group.sorted
38 38 scope = scope.givable unless params[:builtin] == '1'
39 39 @groups = scope.to_a
40 40 }
41 41 end
42 42 end
43 43
44 44 def show
45 45 respond_to do |format|
46 46 format.html
47 47 format.api
48 48 end
49 49 end
50 50
51 51 def new
52 52 @group = Group.new
53 53 end
54 54
55 55 def create
56 56 @group = Group.new
57 57 @group.safe_attributes = params[:group]
58 58
59 59 respond_to do |format|
60 60 if @group.save
61 61 format.html {
62 62 flash[:notice] = l(:notice_successful_create)
63 63 redirect_to(params[:continue] ? new_group_path : groups_path)
64 64 }
65 65 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
66 66 else
67 67 format.html { render :action => "new" }
68 68 format.api { render_validation_errors(@group) }
69 69 end
70 70 end
71 71 end
72 72
73 73 def edit
74 74 end
75 75
76 76 def update
77 77 @group.safe_attributes = params[:group]
78 78
79 79 respond_to do |format|
80 80 if @group.save
81 81 flash[:notice] = l(:notice_successful_update)
82 82 format.html { redirect_to(groups_path) }
83 83 format.api { render_api_ok }
84 84 else
85 85 format.html { render :action => "edit" }
86 86 format.api { render_validation_errors(@group) }
87 87 end
88 88 end
89 89 end
90 90
91 91 def destroy
92 92 @group.destroy
93 93
94 94 respond_to do |format|
95 95 format.html { redirect_to(groups_path) }
96 96 format.api { render_api_ok }
97 97 end
98 98 end
99 99
100 100 def new_users
101 101 end
102 102
103 103 def add_users
104 104 @users = User.not_in_group(@group).where(:id => (params[:user_id] || params[:user_ids])).to_a
105 105 @group.users << @users
106 106 respond_to do |format|
107 107 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
108 108 format.js
109 109 format.api {
110 110 if @users.any?
111 111 render_api_ok
112 112 else
113 113 render_api_errors "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}"
114 114 end
115 115 }
116 116 end
117 117 end
118 118
119 119 def remove_user
120 120 @group.users.delete(User.find(params[:user_id])) if request.delete?
121 121 respond_to do |format|
122 122 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
123 123 format.js
124 124 format.api { render_api_ok }
125 125 end
126 126 end
127 127
128 128 def autocomplete_for_user
129 129 respond_to do |format|
130 130 format.js
131 131 end
132 132 end
133 133
134 134 private
135 135
136 136 def find_group
137 137 @group = Group.find(params[:id])
138 138 rescue ActiveRecord::RecordNotFound
139 139 render_404
140 140 end
141 141
142 142 def user_count_by_group_id
143 143 h = User.joins(:groups).group('group_id').count
144 144 h.keys.each do |key|
145 145 h[key.to_i] = h.delete(key)
146 146 end
147 147 h
148 148 end
149 149 end
@@ -1,124 +1,124
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 'csv'
19 19
20 20 class ImportsController < ApplicationController
21 21
22 22 before_filter :find_import, :only => [:show, :settings, :mapping, :run]
23 23 before_filter :authorize_global
24 24
25 25 helper :issues
26 26
27 27 def new
28 28 end
29 29
30 30 def create
31 31 @import = IssueImport.new
32 32 @import.user = User.current
33 33 @import.file = params[:file]
34 34 @import.set_default_settings
35 35
36 36 if @import.save
37 37 redirect_to import_settings_path(@import)
38 38 else
39 39 render :action => 'new'
40 40 end
41 41 end
42 42
43 43 def show
44 44 end
45 45
46 46 def settings
47 47 if request.post? && @import.parse_file
48 48 redirect_to import_mapping_path(@import)
49 49 end
50 50
51 51 rescue CSV::MalformedCSVError => e
52 52 flash.now[:error] = l(:error_invalid_csv_file_or_settings)
53 53 rescue ArgumentError, Encoding::InvalidByteSequenceError => e
54 54 flash.now[:error] = l(:error_invalid_file_encoding, :encoding => ERB::Util.h(@import.settings['encoding']))
55 55 rescue SystemCallError => e
56 56 flash.now[:error] = l(:error_can_not_read_import_file)
57 57 end
58 58
59 59 def mapping
60 60 issue = Issue.new
61 61 issue.project = @import.project
62 62 issue.tracker = @import.tracker
63 63 @attributes = issue.safe_attribute_names
64 64 @custom_fields = issue.editable_custom_field_values.map(&:custom_field)
65 65
66 66 if request.post?
67 67 respond_to do |format|
68 68 format.html {
69 69 if params[:previous]
70 70 redirect_to import_settings_path(@import)
71 71 else
72 72 redirect_to import_run_path(@import)
73 73 end
74 74 }
75 75 format.js # updates mapping form on project or tracker change
76 76 end
77 77 end
78 78 end
79 79
80 80 def run
81 81 if request.post?
82 82 @current = @import.run(
83 83 :max_items => max_items_per_request,
84 84 :max_time => 10.seconds
85 85 )
86 86 respond_to do |format|
87 87 format.html {
88 88 if @import.finished?
89 89 redirect_to import_path(@import)
90 90 else
91 91 redirect_to import_run_path(@import)
92 92 end
93 93 }
94 94 format.js
95 95 end
96 96 end
97 97 end
98 98
99 99 private
100 100
101 101 def find_import
102 102 @import = Import.where(:user_id => User.current.id, :filename => params[:id]).first
103 103 if @import.nil?
104 104 render_404
105 105 return
106 106 elsif @import.finished? && action_name != 'show'
107 107 redirect_to import_path(@import)
108 108 return
109 109 end
110 110 update_from_params if request.post?
111 111 end
112 112
113 113 def update_from_params
114 114 if params[:import_settings].is_a?(Hash)
115 115 @import.settings ||= {}
116 116 @import.settings.merge!(params[:import_settings])
117 117 @import.save!
118 118 end
119 119 end
120 120
121 121 def max_items_per_request
122 122 5
123 123 end
124 124 end
@@ -1,122 +1,122
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 IssueCategoriesController < ApplicationController
19 19 menu_item :settings
20 20 model_object IssueCategory
21 21 before_filter :find_model_object, :except => [:index, :new, :create]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
24 24 before_filter :authorize
25 25 accept_api_auth :index, :show, :create, :update, :destroy
26 26
27 27 def index
28 28 respond_to do |format|
29 29 format.html { redirect_to_settings_in_projects }
30 30 format.api { @categories = @project.issue_categories.to_a }
31 31 end
32 32 end
33 33
34 34 def show
35 35 respond_to do |format|
36 36 format.html { redirect_to_settings_in_projects }
37 37 format.api
38 38 end
39 39 end
40 40
41 41 def new
42 42 @category = @project.issue_categories.build
43 43 @category.safe_attributes = params[:issue_category]
44 44
45 45 respond_to do |format|
46 46 format.html
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def create
52 52 @category = @project.issue_categories.build
53 53 @category.safe_attributes = params[:issue_category]
54 54 if @category.save
55 55 respond_to do |format|
56 56 format.html do
57 57 flash[:notice] = l(:notice_successful_create)
58 58 redirect_to_settings_in_projects
59 59 end
60 60 format.js
61 61 format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
62 62 end
63 63 else
64 64 respond_to do |format|
65 65 format.html { render :action => 'new'}
66 66 format.js { render :action => 'new'}
67 67 format.api { render_validation_errors(@category) }
68 68 end
69 69 end
70 70 end
71 71
72 72 def edit
73 73 end
74 74
75 75 def update
76 76 @category.safe_attributes = params[:issue_category]
77 77 if @category.save
78 78 respond_to do |format|
79 79 format.html {
80 80 flash[:notice] = l(:notice_successful_update)
81 81 redirect_to_settings_in_projects
82 82 }
83 83 format.api { render_api_ok }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 format.html { render :action => 'edit' }
88 88 format.api { render_validation_errors(@category) }
89 89 end
90 90 end
91 91 end
92 92
93 93 def destroy
94 94 @issue_count = @category.issues.size
95 95 if @issue_count == 0 || params[:todo] || api_request?
96 96 reassign_to = nil
97 97 if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
98 98 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
99 99 end
100 100 @category.destroy(reassign_to)
101 101 respond_to do |format|
102 102 format.html { redirect_to_settings_in_projects }
103 103 format.api { render_api_ok }
104 104 end
105 105 return
106 106 end
107 107 @categories = @project.issue_categories - [@category]
108 108 end
109 109
110 110 private
111 111
112 112 def redirect_to_settings_in_projects
113 113 redirect_to settings_project_path(@project, :tab => 'categories')
114 114 end
115 115
116 116 # Wrap ApplicationController's find_model_object method to set
117 117 # @category instead of just @issue_category
118 118 def find_model_object
119 119 super
120 120 @category = @object
121 121 end
122 122 end
@@ -1,92 +1,92
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :find_issue, :authorize, :only => [:index, :create]
20 20 before_filter :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 28 format.html { render :nothing => true }
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 37 format.html { render :nothing => true }
38 38 format.api
39 39 end
40 40 end
41 41
42 42 def create
43 43 @relation = IssueRelation.new(params[:relation])
44 44 @relation.issue_from = @issue
45 45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
46 46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
47 47 end
48 48 @relation.init_journals(User.current)
49 49 saved = @relation.save
50 50
51 51 respond_to do |format|
52 52 format.html { redirect_to issue_path(@issue) }
53 53 format.js {
54 54 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
55 55 }
56 56 format.api {
57 57 if saved
58 58 render :action => 'show', :status => :created, :location => relation_url(@relation)
59 59 else
60 60 render_validation_errors(@relation)
61 61 end
62 62 }
63 63 end
64 64 end
65 65
66 66 def destroy
67 67 raise Unauthorized unless @relation.deletable?
68 68 @relation.init_journals(User.current)
69 69 @relation.destroy
70 70
71 71 respond_to do |format|
72 72 format.html { redirect_to issue_path(@relation.issue_from) }
73 73 format.js
74 74 format.api { render_api_ok }
75 75 end
76 76 end
77 77
78 78 private
79 79
80 80 def find_issue
81 81 @issue = Issue.find(params[:issue_id])
82 82 @project = @issue.project
83 83 rescue ActiveRecord::RecordNotFound
84 84 render_404
85 85 end
86 86
87 87 def find_relation
88 88 @relation = IssueRelation.find(params[:id])
89 89 rescue ActiveRecord::RecordNotFound
90 90 render_404
91 91 end
92 92 end
@@ -1,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @issue_status_pages, @issue_statuses = paginate IssueStatus.sorted, :per_page => 25
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @issue_statuses = IssueStatus.order('position').to_a
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @issue_status = IssueStatus.new
39 39 end
40 40
41 41 def create
42 42 @issue_status = IssueStatus.new(params[:issue_status])
43 43 if @issue_status.save
44 44 flash[:notice] = l(:notice_successful_create)
45 45 redirect_to issue_statuses_path
46 46 else
47 47 render :action => 'new'
48 48 end
49 49 end
50 50
51 51 def edit
52 52 @issue_status = IssueStatus.find(params[:id])
53 53 end
54 54
55 55 def update
56 56 @issue_status = IssueStatus.find(params[:id])
57 57 if @issue_status.update_attributes(params[:issue_status])
58 58 flash[:notice] = l(:notice_successful_update)
59 59 redirect_to issue_statuses_path(:page => params[:page])
60 60 else
61 61 render :action => 'edit'
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 IssueStatus.find(params[:id]).destroy
67 67 redirect_to issue_statuses_path
68 68 rescue
69 69 flash[:error] = l(:error_unable_delete_issue_status)
70 70 redirect_to issue_statuses_path
71 71 end
72 72
73 73 def update_issue_done_ratio
74 74 if request.post? && IssueStatus.update_issue_done_ratios
75 75 flash[:notice] = l(:notice_issue_done_ratios_updated)
76 76 else
77 77 flash[:error] = l(:error_issue_done_ratios_not_updated)
78 78 end
79 79 redirect_to issue_statuses_path
80 80 end
81 81 end
@@ -1,522 +1,522
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 24 before_filter :authorize, :except => [:index, :new, :create]
25 25 before_filter :find_optional_project, :only => [:index, :new, :create]
26 26 before_filter :build_new_issue_from_params, :only => [:new, :create]
27 27 accept_rss_auth :index, :show
28 28 accept_api_auth :index, :show, :create, :update, :destroy
29 29
30 30 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 31
32 32 helper :journals
33 33 helper :projects
34 34 helper :custom_fields
35 35 helper :issue_relations
36 36 helper :watchers
37 37 helper :attachments
38 38 helper :queries
39 39 include QueriesHelper
40 40 helper :repositories
41 41 helper :sort
42 42 include SortHelper
43 43 helper :timelog
44 44
45 45 def index
46 46 retrieve_query
47 47 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
48 48 sort_update(@query.sortable_columns)
49 49 @query.sort_criteria = sort_criteria.to_a
50 50
51 51 if @query.valid?
52 52 case params[:format]
53 53 when 'csv', 'pdf'
54 54 @limit = Setting.issues_export_limit.to_i
55 55 if params[:columns] == 'all'
56 56 @query.column_names = @query.available_inline_columns.map(&:name)
57 57 end
58 58 when 'atom'
59 59 @limit = Setting.feeds_limit.to_i
60 60 when 'xml', 'json'
61 61 @offset, @limit = api_offset_and_limit
62 62 @query.column_names = %w(author)
63 63 else
64 64 @limit = per_page_option
65 65 end
66 66
67 67 @issue_count = @query.issue_count
68 68 @issue_pages = Paginator.new @issue_count, @limit, params['page']
69 69 @offset ||= @issue_pages.offset
70 70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 71 :order => sort_clause,
72 72 :offset => @offset,
73 73 :limit => @limit)
74 74 @issue_count_by_group = @query.issue_count_by_group
75 75
76 76 respond_to do |format|
77 77 format.html { render :template => 'issues/index', :layout => !request.xhr? }
78 78 format.api {
79 79 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
80 80 }
81 81 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
82 82 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
83 83 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
88 88 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
89 89 format.api { render_validation_errors(@query) }
90 90 end
91 91 end
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def show
97 97 @journals = @issue.journals.includes(:user, :details).
98 98 references(:user, :details).
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) }
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 end
216 216
217 217 @allowed_projects = Issue.allowed_target_projects
218 218 if params[:issue]
219 219 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 220 if @target_project
221 221 target_projects = [@target_project]
222 222 end
223 223 end
224 224 target_projects ||= @projects
225 225
226 226 if @copy
227 227 # Copied issues will get their default statuses
228 228 @available_statuses = []
229 229 else
230 230 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
231 231 end
232 232 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
233 233 @assignables = target_projects.map(&:assignable_users).reduce(:&)
234 234 @trackers = target_projects.map(&:trackers).reduce(:&)
235 235 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
236 236 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
237 237 if @copy
238 238 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
239 239 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
240 240 end
241 241
242 242 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
243 243
244 244 @issue_params = params[:issue] || {}
245 245 @issue_params[:custom_field_values] ||= {}
246 246 end
247 247
248 248 def bulk_update
249 249 @issues.sort!
250 250 @copy = params[:copy].present?
251 251
252 252 attributes = parse_params_for_bulk_issue_attributes(params)
253 253 copy_subtasks = (params[:copy_subtasks] == '1')
254 254 copy_attachments = (params[:copy_attachments] == '1')
255 255
256 256 if @copy
257 257 unless User.current.allowed_to?(:copy_issues, @projects)
258 258 raise ::Unauthorized
259 259 end
260 260 target_projects = @projects
261 261 if attributes['project_id'].present?
262 262 target_projects = Project.where(:id => attributes['project_id']).to_a
263 263 end
264 264 unless User.current.allowed_to?(:add_issues, target_projects)
265 265 raise ::Unauthorized
266 266 end
267 267 end
268 268
269 269 unsaved_issues = []
270 270 saved_issues = []
271 271
272 272 if @copy && copy_subtasks
273 273 # Descendant issues will be copied with the parent task
274 274 # Don't copy them twice
275 275 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
276 276 end
277 277
278 278 @issues.each do |orig_issue|
279 279 orig_issue.reload
280 280 if @copy
281 281 issue = orig_issue.copy({},
282 282 :attachments => copy_attachments,
283 283 :subtasks => copy_subtasks,
284 284 :link => link_copy?(params[:link_copy])
285 285 )
286 286 else
287 287 issue = orig_issue
288 288 end
289 289 journal = issue.init_journal(User.current, params[:notes])
290 290 issue.safe_attributes = attributes
291 291 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
292 292 if issue.save
293 293 saved_issues << issue
294 294 else
295 295 unsaved_issues << orig_issue
296 296 end
297 297 end
298 298
299 299 if unsaved_issues.empty?
300 300 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
301 301 if params[:follow]
302 302 if @issues.size == 1 && saved_issues.size == 1
303 303 redirect_to issue_path(saved_issues.first)
304 304 elsif saved_issues.map(&:project).uniq.size == 1
305 305 redirect_to project_issues_path(saved_issues.map(&:project).first)
306 306 end
307 307 else
308 308 redirect_back_or_default _project_issues_path(@project)
309 309 end
310 310 else
311 311 @saved_issues = @issues
312 312 @unsaved_issues = unsaved_issues
313 313 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
314 314 bulk_edit
315 315 render :action => 'bulk_edit'
316 316 end
317 317 end
318 318
319 319 def destroy
320 320 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
321 321 if @hours > 0
322 322 case params[:todo]
323 323 when 'destroy'
324 324 # nothing to do
325 325 when 'nullify'
326 326 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
327 327 when 'reassign'
328 328 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
329 329 if reassign_to.nil?
330 330 flash.now[:error] = l(:error_issue_not_found_in_project)
331 331 return
332 332 else
333 333 TimeEntry.where(['issue_id IN (?)', @issues]).
334 334 update_all("issue_id = #{reassign_to.id}")
335 335 end
336 336 else
337 337 # display the destroy form if it's a user request
338 338 return unless api_request?
339 339 end
340 340 end
341 341 @issues.each do |issue|
342 342 begin
343 343 issue.reload.destroy
344 344 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
345 345 # nothing to do, issue was already deleted (eg. by a parent)
346 346 end
347 347 end
348 348 respond_to do |format|
349 349 format.html { redirect_back_or_default _project_issues_path(@project) }
350 350 format.api { render_api_ok }
351 351 end
352 352 end
353 353
354 354 private
355 355
356 356 def retrieve_previous_and_next_issue_ids
357 357 retrieve_query_from_session
358 358 if @query
359 359 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
360 360 sort_update(@query.sortable_columns, 'issues_index_sort')
361 361 limit = 500
362 362 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
363 363 if (idx = issue_ids.index(@issue.id)) && idx < limit
364 364 if issue_ids.size < 500
365 365 @issue_position = idx + 1
366 366 @issue_count = issue_ids.size
367 367 end
368 368 @prev_issue_id = issue_ids[idx - 1] if idx > 0
369 369 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
370 370 end
371 371 end
372 372 end
373 373
374 374 # Used by #edit and #update to set some common instance variables
375 375 # from the params
376 376 def update_issue_from_params
377 377 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
378 378 if params[:time_entry]
379 379 @time_entry.safe_attributes = params[:time_entry]
380 380 end
381 381
382 382 @issue.init_journal(User.current)
383 383
384 384 issue_attributes = params[:issue]
385 385 if issue_attributes && params[:conflict_resolution]
386 386 case params[:conflict_resolution]
387 387 when 'overwrite'
388 388 issue_attributes = issue_attributes.dup
389 389 issue_attributes.delete(:lock_version)
390 390 when 'add_notes'
391 391 issue_attributes = issue_attributes.slice(:notes, :private_notes)
392 392 when 'cancel'
393 393 redirect_to issue_path(@issue)
394 394 return false
395 395 end
396 396 end
397 397 @issue.safe_attributes = issue_attributes
398 398 @priorities = IssuePriority.active
399 399 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
400 400 true
401 401 end
402 402
403 403 # Used by #new and #create to build a new issue from the params
404 404 # The new issue will be copied from an existing one if copy_from parameter is given
405 405 def build_new_issue_from_params
406 406 @issue = Issue.new
407 407 if params[:copy_from]
408 408 begin
409 409 @issue.init_journal(User.current)
410 410 @copy_from = Issue.visible.find(params[:copy_from])
411 411 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
412 412 raise ::Unauthorized
413 413 end
414 414 @link_copy = link_copy?(params[:link_copy]) || request.get?
415 415 @copy_attachments = params[:copy_attachments].present? || request.get?
416 416 @copy_subtasks = params[:copy_subtasks].present? || request.get?
417 417 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
418 418 rescue ActiveRecord::RecordNotFound
419 419 render_404
420 420 return
421 421 end
422 422 end
423 423 @issue.project = @project
424 424 if request.get?
425 425 @issue.project ||= @issue.allowed_target_projects.first
426 426 end
427 427 @issue.author ||= User.current
428 428 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
429 429
430 430 attrs = (params[:issue] || {}).deep_dup
431 431 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
432 432 attrs.delete(:status_id)
433 433 end
434 434 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
435 435 # Discard submitted version when changing the project on the issue form
436 436 # so we can use the default version for the new project
437 437 attrs.delete(:fixed_version_id)
438 438 end
439 439 @issue.safe_attributes = attrs
440 440
441 441 if @issue.project
442 442 @issue.tracker ||= @issue.project.trackers.first
443 443 if @issue.tracker.nil?
444 444 render_error l(:error_no_tracker_in_project)
445 445 return false
446 446 end
447 447 if @issue.status.nil?
448 448 render_error l(:error_no_default_issue_status)
449 449 return false
450 450 end
451 451 end
452 452
453 453 @priorities = IssuePriority.active
454 454 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
455 455 end
456 456
457 457 def parse_params_for_bulk_issue_attributes(params)
458 458 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
459 459 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
460 460 if custom = attributes[:custom_field_values]
461 461 custom.reject! {|k,v| v.blank?}
462 462 custom.keys.each do |k|
463 463 if custom[k].is_a?(Array)
464 464 custom[k] << '' if custom[k].delete('__none__')
465 465 else
466 466 custom[k] = '' if custom[k] == '__none__'
467 467 end
468 468 end
469 469 end
470 470 attributes
471 471 end
472 472
473 473 # Saves @issue and a time_entry from the parameters
474 474 def save_issue_with_child_records
475 475 Issue.transaction do
476 476 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
477 477 time_entry = @time_entry || TimeEntry.new
478 478 time_entry.project = @issue.project
479 479 time_entry.issue = @issue
480 480 time_entry.user = User.current
481 481 time_entry.spent_on = User.current.today
482 482 time_entry.attributes = params[:time_entry]
483 483 @issue.time_entries << time_entry
484 484 end
485 485
486 486 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
487 487 if @issue.save
488 488 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
489 489 else
490 490 raise ActiveRecord::Rollback
491 491 end
492 492 end
493 493 end
494 494
495 495 # Returns true if the issue copy should be linked
496 496 # to the original issue
497 497 def link_copy?(param)
498 498 case Setting.link_copied_issue
499 499 when 'yes'
500 500 true
501 501 when 'no'
502 502 false
503 503 when 'ask'
504 504 param == '1'
505 505 end
506 506 end
507 507
508 508 # Redirects user after a successful issue creation
509 509 def redirect_after_create
510 510 if params[:continue]
511 511 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
512 512 if params[:project_id]
513 513 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
514 514 else
515 515 attrs.merge! :project_id => @issue.project_id
516 516 redirect_to new_issue_path(:issue => attrs)
517 517 end
518 518 else
519 519 redirect_to issue_path(@issue)
520 520 end
521 521 end
522 522 end
@@ -1,109 +1,109
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 JournalsController < ApplicationController
19 19 before_filter :find_journal, :only => [:edit, :diff]
20 20 before_filter :find_issue, :only => [:new]
21 21 before_filter :find_optional_project, :only => [:index]
22 22 before_filter :authorize, :only => [:new, :edit, :diff]
23 23 accept_rss_auth :index
24 24 menu_item :issues
25 25
26 26 helper :issues
27 27 helper :custom_fields
28 28 helper :queries
29 29 include QueriesHelper
30 30 helper :sort
31 31 include SortHelper
32 32
33 33 def index
34 34 retrieve_query
35 35 sort_init 'id', 'desc'
36 36 sort_update(@query.sortable_columns)
37 37 if @query.valid?
38 38 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
39 39 :limit => 25)
40 40 end
41 41 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
42 42 render :layout => false, :content_type => 'application/atom+xml'
43 43 rescue ActiveRecord::RecordNotFound
44 44 render_404
45 45 end
46 46
47 47 def diff
48 48 @issue = @journal.issue
49 49 if params[:detail_id].present?
50 50 @detail = @journal.details.find_by_id(params[:detail_id])
51 51 else
52 52 @detail = @journal.details.detect {|d| d.property == 'attr' && d.prop_key == 'description'}
53 53 end
54 54 unless @issue && @detail
55 55 render_404
56 56 return false
57 57 end
58 58 if @detail.property == 'cf'
59 59 unless @detail.custom_field && @detail.custom_field.visible_by?(@issue.project, User.current)
60 60 raise ::Unauthorized
61 61 end
62 62 end
63 63 @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
64 64 end
65 65
66 66 def new
67 67 @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
68 68 if @journal
69 69 user = @journal.user
70 70 text = @journal.notes
71 71 else
72 72 user = @issue.author
73 73 text = @issue.description
74 74 end
75 75 # Replaces pre blocks with [...]
76 76 text = text.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]')
77 77 @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
78 78 @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
79 79 rescue ActiveRecord::RecordNotFound
80 80 render_404
81 81 end
82 82
83 83 def edit
84 84 (render_403; return false) unless @journal.editable_by?(User.current)
85 85 if request.post?
86 86 @journal.update_attributes(:notes => params[:notes]) if params[:notes]
87 87 @journal.destroy if @journal.details.empty? && @journal.notes.blank?
88 88 call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
89 89 respond_to do |format|
90 90 format.html { redirect_to issue_path(@journal.journalized) }
91 91 format.js { render :action => 'update' }
92 92 end
93 93 else
94 94 respond_to do |format|
95 95 # TODO: implement non-JS journal update
96 96 format.js
97 97 end
98 98 end
99 99 end
100 100
101 101 private
102 102
103 103 def find_journal
104 104 @journal = Journal.visible.find(params[:id])
105 105 @project = @journal.journalized.project
106 106 rescue ActiveRecord::RecordNotFound
107 107 render_404
108 108 end
109 109 end
@@ -1,44 +1,44
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :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 30 render :nothing => true, :status => :created
31 31 else
32 32 render :nothing => true, :status => :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,129 +1,129
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 MembersController < ApplicationController
19 19 model_object Member
20 20 before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete]
23 23 before_filter :authorize
24 24 accept_api_auth :index, :show, :create, :update, :destroy
25 25
26 26 require_sudo_mode :create, :update, :destroy
27 27
28 28 def index
29 29 scope = @project.memberships.active
30 30 @offset, @limit = api_offset_and_limit
31 31 @member_count = scope.count
32 32 @member_pages = Paginator.new @member_count, @limit, params['page']
33 33 @offset ||= @member_pages.offset
34 34 @members = scope.order(:id).limit(@limit).offset(@offset).to_a
35 35
36 36 respond_to do |format|
37 37 format.html { head 406 }
38 38 format.api
39 39 end
40 40 end
41 41
42 42 def show
43 43 respond_to do |format|
44 44 format.html { head 406 }
45 45 format.api
46 46 end
47 47 end
48 48
49 49 def new
50 50 @member = Member.new
51 51 end
52 52
53 53 def create
54 54 members = []
55 55 if params[:membership]
56 56 user_ids = Array.wrap(params[:membership][:user_id] || params[:membership][:user_ids])
57 57 user_ids << nil if user_ids.empty?
58 58 user_ids.each do |user_id|
59 59 member = Member.new(:project => @project, :user_id => user_id)
60 60 member.set_editable_role_ids(params[:membership][:role_ids])
61 61 members << member
62 62 end
63 63 @project.members << members
64 64 end
65 65
66 66 respond_to do |format|
67 67 format.html { redirect_to_settings_in_projects }
68 68 format.js {
69 69 @members = members
70 70 @member = Member.new
71 71 }
72 72 format.api {
73 73 @member = members.first
74 74 if @member.valid?
75 75 render :action => 'show', :status => :created, :location => membership_url(@member)
76 76 else
77 77 render_validation_errors(@member)
78 78 end
79 79 }
80 80 end
81 81 end
82 82
83 83 def update
84 84 if params[:membership]
85 85 @member.set_editable_role_ids(params[:membership][:role_ids])
86 86 end
87 87 saved = @member.save
88 88 respond_to do |format|
89 89 format.html { redirect_to_settings_in_projects }
90 90 format.js
91 91 format.api {
92 92 if saved
93 93 render_api_ok
94 94 else
95 95 render_validation_errors(@member)
96 96 end
97 97 }
98 98 end
99 99 end
100 100
101 101 def destroy
102 102 if @member.deletable?
103 103 @member.destroy
104 104 end
105 105 respond_to do |format|
106 106 format.html { redirect_to_settings_in_projects }
107 107 format.js
108 108 format.api {
109 109 if @member.destroyed?
110 110 render_api_ok
111 111 else
112 112 head :unprocessable_entity
113 113 end
114 114 }
115 115 end
116 116 end
117 117
118 118 def autocomplete
119 119 respond_to do |format|
120 120 format.js
121 121 end
122 122 end
123 123
124 124 private
125 125
126 126 def redirect_to_settings_in_projects
127 127 redirect_to settings_project_path(@project, :tab => 'members')
128 128 end
129 129 end
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 MessagesController < ApplicationController
19 19 menu_item :boards
20 20 default_search_scope :messages
21 21 before_filter :find_board, :only => [:new, :preview]
22 22 before_filter :find_attachments, :only => [:preview]
23 23 before_filter :find_message, :except => [:new, :preview]
24 24 before_filter :authorize, :except => [:preview, :edit, :destroy]
25 25
26 26 helper :boards
27 27 helper :watchers
28 28 helper :attachments
29 29 include AttachmentsHelper
30 30
31 31 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
32 32
33 33 # Show a topic and its replies
34 34 def show
35 35 page = params[:page]
36 36 # Find the page of the requested reply
37 37 if params[:r] && page.nil?
38 38 offset = @topic.children.where("#{Message.table_name}.id < ?", params[:r].to_i).count
39 39 page = 1 + offset / REPLIES_PER_PAGE
40 40 end
41 41
42 42 @reply_count = @topic.children.count
43 43 @reply_pages = Paginator.new @reply_count, REPLIES_PER_PAGE, page
44 44 @replies = @topic.children.
45 45 includes(:author, :attachments, {:board => :project}).
46 46 reorder("#{Message.table_name}.created_on ASC, #{Message.table_name}.id ASC").
47 47 limit(@reply_pages.per_page).
48 48 offset(@reply_pages.offset).
49 49 to_a
50 50
51 51 @reply = Message.new(:subject => "RE: #{@message.subject}")
52 52 render :action => "show", :layout => false if request.xhr?
53 53 end
54 54
55 55 # Create a new topic
56 56 def new
57 57 @message = Message.new
58 58 @message.author = User.current
59 59 @message.board = @board
60 60 @message.safe_attributes = params[:message]
61 61 if request.post?
62 62 @message.save_attachments(params[:attachments])
63 63 if @message.save
64 64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
65 65 render_attachment_warning_if_needed(@message)
66 66 redirect_to board_message_path(@board, @message)
67 67 end
68 68 end
69 69 end
70 70
71 71 # Reply to a topic
72 72 def reply
73 73 @reply = Message.new
74 74 @reply.author = User.current
75 75 @reply.board = @board
76 76 @reply.safe_attributes = params[:reply]
77 77 @topic.children << @reply
78 78 if !@reply.new_record?
79 79 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
80 80 attachments = Attachment.attach_files(@reply, params[:attachments])
81 81 render_attachment_warning_if_needed(@reply)
82 82 end
83 83 redirect_to board_message_path(@board, @topic, :r => @reply)
84 84 end
85 85
86 86 # Edit a message
87 87 def edit
88 88 (render_403; return false) unless @message.editable_by?(User.current)
89 89 @message.safe_attributes = params[:message]
90 90 if request.post? && @message.save
91 91 attachments = Attachment.attach_files(@message, params[:attachments])
92 92 render_attachment_warning_if_needed(@message)
93 93 flash[:notice] = l(:notice_successful_update)
94 94 @message.reload
95 95 redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id))
96 96 end
97 97 end
98 98
99 99 # Delete a messages
100 100 def destroy
101 101 (render_403; return false) unless @message.destroyable_by?(User.current)
102 102 r = @message.to_param
103 103 @message.destroy
104 104 if @message.parent
105 105 redirect_to board_message_path(@board, @message.parent, :r => r)
106 106 else
107 107 redirect_to project_board_path(@project, @board)
108 108 end
109 109 end
110 110
111 111 def quote
112 112 @subject = @message.subject
113 113 @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:')
114 114
115 115 @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> "
116 116 @content << @message.content.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
117 117 end
118 118
119 119 def preview
120 120 message = @board.messages.find_by_id(params[:id])
121 121 @text = (params[:message] || params[:reply])[:content]
122 122 @previewed = message
123 123 render :partial => 'common/preview'
124 124 end
125 125
126 126 private
127 127 def find_message
128 128 return unless find_board
129 129 @message = @board.messages.includes(:parent).find(params[:id])
130 130 @topic = @message.root
131 131 rescue ActiveRecord::RecordNotFound
132 132 render_404
133 133 end
134 134
135 135 def find_board
136 136 @board = Board.includes(:project).find(params[:board_id])
137 137 @project = @board.project
138 138 rescue ActiveRecord::RecordNotFound
139 139 render_404
140 140 nil
141 141 end
142 142 end
@@ -1,210 +1,210
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_login
20 20 # let user change user's password when user has to
21 21 skip_before_filter :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 flash[:notice] = l(:notice_account_password_updated)
109 109 redirect_to my_account_path
110 110 end
111 111 end
112 112 end
113 113 end
114 114
115 115 # Create a new feeds key
116 116 def reset_rss_key
117 117 if request.post?
118 118 if User.current.rss_token
119 119 User.current.rss_token.destroy
120 120 User.current.reload
121 121 end
122 122 User.current.rss_key
123 123 flash[:notice] = l(:notice_feeds_access_key_reseted)
124 124 end
125 125 redirect_to my_account_path
126 126 end
127 127
128 128 def show_api_key
129 129 @user = User.current
130 130 end
131 131
132 132 # Create a new API key
133 133 def reset_api_key
134 134 if request.post?
135 135 if User.current.api_token
136 136 User.current.api_token.destroy
137 137 User.current.reload
138 138 end
139 139 User.current.api_key
140 140 flash[:notice] = l(:notice_api_access_key_reseted)
141 141 end
142 142 redirect_to my_account_path
143 143 end
144 144
145 145 # User's page layout configuration
146 146 def page_layout
147 147 @user = User.current
148 148 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
149 149 @block_options = []
150 150 BLOCKS.each do |k, v|
151 151 unless @blocks.values.flatten.include?(k)
152 152 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
153 153 end
154 154 end
155 155 end
156 156
157 157 # Add a block to user's page
158 158 # The block is added on top of the page
159 159 # params[:block] : id of the block to add
160 160 def add_block
161 161 block = params[:block].to_s.underscore
162 162 if block.present? && BLOCKS.key?(block)
163 163 @user = User.current
164 164 layout = @user.pref[:my_page_layout] || {}
165 165 # remove if already present in a group
166 166 %w(top left right).each {|f| (layout[f] ||= []).delete block }
167 167 # add it on top
168 168 layout['top'].unshift block
169 169 @user.pref[:my_page_layout] = layout
170 170 @user.pref.save
171 171 end
172 172 redirect_to my_page_layout_path
173 173 end
174 174
175 175 # Remove a block to user's page
176 176 # params[:block] : id of the block to remove
177 177 def remove_block
178 178 block = params[:block].to_s.underscore
179 179 @user = User.current
180 180 # remove block in all groups
181 181 layout = @user.pref[:my_page_layout] || {}
182 182 %w(top left right).each {|f| (layout[f] ||= []).delete block }
183 183 @user.pref[:my_page_layout] = layout
184 184 @user.pref.save
185 185 redirect_to my_page_layout_path
186 186 end
187 187
188 188 # Change blocks order on user's page
189 189 # params[:group] : group to order (top, left or right)
190 190 # params[:list-(top|left|right)] : array of block ids of the group
191 191 def order_blocks
192 192 group = params[:group]
193 193 @user = User.current
194 194 if group.is_a?(String)
195 195 group_items = (params["blocks"] || []).collect(&:underscore)
196 196 group_items.each {|s| s.sub!(/^block_/, '')}
197 197 if group_items and group_items.is_a? Array
198 198 layout = @user.pref[:my_page_layout] || {}
199 199 # remove group blocks if they are presents in other groups
200 200 %w(top left right).each {|f|
201 201 layout[f] = (layout[f] || []) - group_items
202 202 }
203 203 layout[group] = group_items
204 204 @user.pref[:my_page_layout] = layout
205 205 @user.pref.save
206 206 end
207 207 end
208 208 render :nothing => true
209 209 end
210 210 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 NewsController < ApplicationController
19 19 default_search_scope :news
20 20 model_object News
21 21 before_filter :find_model_object, :except => [:new, :create, :index]
22 22 before_filter :find_project_from_association, :except => [:new, :create, :index]
23 23 before_filter :find_project_by_project_id, :only => [:new, :create]
24 24 before_filter :authorize, :except => [:index]
25 25 before_filter :find_optional_project, :only => :index
26 26 accept_rss_auth :index
27 27 accept_api_auth :index
28 28
29 29 helper :watchers
30 30 helper :attachments
31 31
32 32 def index
33 33 case params[:format]
34 34 when 'xml', 'json'
35 35 @offset, @limit = api_offset_and_limit
36 36 else
37 37 @limit = 10
38 38 end
39 39
40 40 scope = @project ? @project.news.visible : News.visible
41 41
42 42 @news_count = scope.count
43 43 @news_pages = Paginator.new @news_count, @limit, params['page']
44 44 @offset ||= @news_pages.offset
45 45 @newss = scope.includes([:author, :project]).
46 46 order("#{News.table_name}.created_on DESC").
47 47 limit(@limit).
48 48 offset(@offset).
49 49 to_a
50 50 respond_to do |format|
51 51 format.html {
52 52 @news = News.new # for adding news inline
53 53 render :layout => false if request.xhr?
54 54 }
55 55 format.api
56 56 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
57 57 end
58 58 end
59 59
60 60 def show
61 61 @comments = @news.comments.to_a
62 62 @comments.reverse! if User.current.wants_comments_in_reverse_order?
63 63 end
64 64
65 65 def new
66 66 @news = News.new(:project => @project, :author => User.current)
67 67 end
68 68
69 69 def create
70 70 @news = News.new(:project => @project, :author => User.current)
71 71 @news.safe_attributes = params[:news]
72 72 @news.save_attachments(params[:attachments])
73 73 if @news.save
74 74 render_attachment_warning_if_needed(@news)
75 75 flash[:notice] = l(:notice_successful_create)
76 76 redirect_to project_news_index_path(@project)
77 77 else
78 78 render :action => 'new'
79 79 end
80 80 end
81 81
82 82 def edit
83 83 end
84 84
85 85 def update
86 86 @news.safe_attributes = params[:news]
87 87 @news.save_attachments(params[:attachments])
88 88 if @news.save
89 89 render_attachment_warning_if_needed(@news)
90 90 flash[:notice] = l(:notice_successful_update)
91 91 redirect_to news_path(@news)
92 92 else
93 93 render :action => 'edit'
94 94 end
95 95 end
96 96
97 97 def destroy
98 98 @news.destroy
99 99 redirect_to project_news_index_path(@project)
100 100 end
101 101
102 102 private
103 103
104 104 def find_optional_project
105 105 return true unless params[:project_id]
106 106 @project = Project.find(params[:project_id])
107 107 authorize
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 PreviewsController < ApplicationController
19 19 before_filter :find_project, :find_attachments
20 20
21 21 def issue
22 22 @issue = Issue.visible.find_by_id(params[:id]) unless params[:id].blank?
23 23 if @issue
24 24 @description = params[:issue] && params[:issue][:description]
25 25 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
26 26 @description = nil
27 27 end
28 28 # params[:notes] is useful for preview of notes in issue history
29 29 @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil)
30 30 else
31 31 @description = (params[:issue] ? params[:issue][:description] : nil)
32 32 end
33 33 render :layout => false
34 34 end
35 35
36 36 def news
37 37 if params[:id].present? && news = News.visible.find_by_id(params[:id])
38 38 @previewed = news
39 39 end
40 40 @text = (params[:news] ? params[:news][:description] : nil)
41 41 render :partial => 'common/preview'
42 42 end
43 43
44 44 private
45 45
46 46 def find_project
47 47 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
48 48 @project = Project.find(project_id)
49 49 rescue ActiveRecord::RecordNotFound
50 50 render_404
51 51 end
52 52
53 53 end
@@ -1,80 +1,80
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 PrincipalMembershipsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_principal, :only => [:new, :create]
23 23 before_filter :find_membership, :only => [:update, :destroy]
24 24
25 25 def new
26 26 @projects = Project.active.all
27 27 @roles = Role.find_all_givable
28 28 respond_to do |format|
29 29 format.html
30 30 format.js
31 31 end
32 32 end
33 33
34 34 def create
35 35 @members = Member.create_principal_memberships(@principal, params[:membership])
36 36 respond_to do |format|
37 37 format.html { redirect_to_principal @principal }
38 38 format.js
39 39 end
40 40 end
41 41
42 42 def update
43 43 @membership.attributes = params[:membership]
44 44 @membership.save
45 45 respond_to do |format|
46 46 format.html { redirect_to_principal @principal }
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def destroy
52 52 if @membership.deletable?
53 53 @membership.destroy
54 54 end
55 55 respond_to do |format|
56 56 format.html { redirect_to_principal @principal }
57 57 format.js
58 58 end
59 59 end
60 60
61 61 private
62 62
63 63 def find_principal
64 64 principal_id = params[:user_id] || params[:group_id]
65 65 @principal = Principal.find(principal_id)
66 66 rescue ActiveRecord::RecordNotFound
67 67 render_404
68 68 end
69 69
70 70 def find_membership
71 71 @membership = Member.find(params[:id])
72 72 @principal = @membership.principal
73 73 rescue ActiveRecord::RecordNotFound
74 74 render_404
75 75 end
76 76
77 77 def redirect_to_principal(principal)
78 78 redirect_to edit_polymorphic_path(principal, :tab => 'memberships')
79 79 end
80 80 end
@@ -1,44 +1,44
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 ProjectEnumerationsController < ApplicationController
19 19 before_filter :find_project_by_project_id
20 20 before_filter :authorize
21 21
22 22 def update
23 23 if params[:enumerations]
24 24 saved = Project.transaction do
25 25 params[:enumerations].each do |id, activity|
26 26 @project.update_or_create_time_entry_activity(id, activity)
27 27 end
28 28 end
29 29 if saved
30 30 flash[:notice] = l(:notice_successful_update)
31 31 end
32 32 end
33 33
34 34 redirect_to settings_project_path(@project, :tab => 'activities')
35 35 end
36 36
37 37 def destroy
38 38 @project.time_entry_activities.each do |time_entry_activity|
39 39 time_entry_activity.destroy(time_entry_activity.parent)
40 40 end
41 41 flash[:notice] = l(:notice_successful_update)
42 42 redirect_to settings_project_path(@project, :tab => 'activities')
43 43 end
44 44 end
@@ -1,234 +1,234
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :settings, :only => :settings
21 21
22 22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 24 before_filter :authorize_global, :only => [:new, :create]
25 25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 26 accept_rss_auth :index
27 27 accept_api_auth :index, :show, :create, :update, :destroy
28 28 require_sudo_mode :destroy
29 29
30 30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
31 31 if controller.request.post?
32 32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
33 33 end
34 34 end
35 35
36 36 helper :custom_fields
37 37 helper :issues
38 38 helper :queries
39 39 helper :repositories
40 40 helper :members
41 41
42 42 # Lists visible projects
43 43 def index
44 44 scope = Project.visible.sorted
45 45
46 46 respond_to do |format|
47 47 format.html {
48 48 unless params[:closed]
49 49 scope = scope.active
50 50 end
51 51 @projects = scope.to_a
52 52 }
53 53 format.api {
54 54 @offset, @limit = api_offset_and_limit
55 55 @project_count = scope.count
56 56 @projects = scope.offset(@offset).limit(@limit).to_a
57 57 }
58 58 format.atom {
59 59 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
60 60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 61 }
62 62 end
63 63 end
64 64
65 65 def new
66 66 @issue_custom_fields = IssueCustomField.sorted.to_a
67 67 @trackers = Tracker.sorted.to_a
68 68 @project = Project.new
69 69 @project.safe_attributes = params[:project]
70 70 end
71 71
72 72 def create
73 73 @issue_custom_fields = IssueCustomField.sorted.to_a
74 74 @trackers = Tracker.sorted.to_a
75 75 @project = Project.new
76 76 @project.safe_attributes = params[:project]
77 77
78 78 if @project.save
79 79 unless User.current.admin?
80 80 @project.add_default_member(User.current)
81 81 end
82 82 respond_to do |format|
83 83 format.html {
84 84 flash[:notice] = l(:notice_successful_create)
85 85 if params[:continue]
86 86 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
87 87 redirect_to new_project_path(attrs)
88 88 else
89 89 redirect_to settings_project_path(@project)
90 90 end
91 91 }
92 92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
93 93 end
94 94 else
95 95 respond_to do |format|
96 96 format.html { render :action => 'new' }
97 97 format.api { render_validation_errors(@project) }
98 98 end
99 99 end
100 100 end
101 101
102 102 def copy
103 103 @issue_custom_fields = IssueCustomField.sorted.to_a
104 104 @trackers = Tracker.sorted.to_a
105 105 @source_project = Project.find(params[:id])
106 106 if request.get?
107 107 @project = Project.copy_from(@source_project)
108 108 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
109 109 else
110 110 Mailer.with_deliveries(params[:notifications] == '1') do
111 111 @project = Project.new
112 112 @project.safe_attributes = params[:project]
113 113 if @project.copy(@source_project, :only => params[:only])
114 114 flash[:notice] = l(:notice_successful_create)
115 115 redirect_to settings_project_path(@project)
116 116 elsif !@project.new_record?
117 117 # Project was created
118 118 # But some objects were not copied due to validation failures
119 119 # (eg. issues from disabled trackers)
120 120 # TODO: inform about that
121 121 redirect_to settings_project_path(@project)
122 122 end
123 123 end
124 124 end
125 125 rescue ActiveRecord::RecordNotFound
126 126 # source_project not found
127 127 render_404
128 128 end
129 129
130 130 # Show @project
131 131 def show
132 132 # try to redirect to the requested menu item
133 133 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
134 134 return
135 135 end
136 136
137 137 @users_by_role = @project.users_by_role
138 138 @subprojects = @project.children.visible.to_a
139 139 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
140 140 @trackers = @project.rolled_up_trackers
141 141
142 142 cond = @project.project_condition(Setting.display_subprojects_issues?)
143 143
144 144 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
145 145 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
146 146
147 147 if User.current.allowed_to_view_all_time_entries?(@project)
148 148 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
149 149 end
150 150
151 151 @key = User.current.rss_key
152 152
153 153 respond_to do |format|
154 154 format.html
155 155 format.api
156 156 end
157 157 end
158 158
159 159 def settings
160 160 @issue_custom_fields = IssueCustomField.sorted.to_a
161 161 @issue_category ||= IssueCategory.new
162 162 @member ||= @project.members.new
163 163 @trackers = Tracker.sorted.to_a
164 164 @wiki ||= @project.wiki || Wiki.new(:project => @project)
165 165 end
166 166
167 167 def edit
168 168 end
169 169
170 170 def update
171 171 @project.safe_attributes = params[:project]
172 172 if @project.save
173 173 respond_to do |format|
174 174 format.html {
175 175 flash[:notice] = l(:notice_successful_update)
176 176 redirect_to settings_project_path(@project)
177 177 }
178 178 format.api { render_api_ok }
179 179 end
180 180 else
181 181 respond_to do |format|
182 182 format.html {
183 183 settings
184 184 render :action => 'settings'
185 185 }
186 186 format.api { render_validation_errors(@project) }
187 187 end
188 188 end
189 189 end
190 190
191 191 def modules
192 192 @project.enabled_module_names = params[:enabled_module_names]
193 193 flash[:notice] = l(:notice_successful_update)
194 194 redirect_to settings_project_path(@project, :tab => 'modules')
195 195 end
196 196
197 197 def archive
198 198 unless @project.archive
199 199 flash[:error] = l(:error_can_not_archive_project)
200 200 end
201 201 redirect_to admin_projects_path(:status => params[:status])
202 202 end
203 203
204 204 def unarchive
205 205 unless @project.active?
206 206 @project.unarchive
207 207 end
208 208 redirect_to admin_projects_path(:status => params[:status])
209 209 end
210 210
211 211 def close
212 212 @project.close
213 213 redirect_to project_path(@project)
214 214 end
215 215
216 216 def reopen
217 217 @project.reopen
218 218 redirect_to project_path(@project)
219 219 end
220 220
221 221 # Delete @project
222 222 def destroy
223 223 @project_to_destroy = @project
224 224 if api_request? || params[:confirm]
225 225 @project_to_destroy.destroy
226 226 respond_to do |format|
227 227 format.html { redirect_to admin_projects_path }
228 228 format.api { render_api_ok }
229 229 end
230 230 end
231 231 # hide project in layout
232 232 @project = nil
233 233 end
234 234 end
@@ -1,129 +1,129
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => [:new, :create, :index]
21 21 before_filter :find_optional_project, :only => [:new, :create]
22 22
23 23 accept_api_auth :index
24 24
25 25 include QueriesHelper
26 26
27 27 def index
28 28 case params[:format]
29 29 when 'xml', 'json'
30 30 @offset, @limit = api_offset_and_limit
31 31 else
32 32 @limit = per_page_option
33 33 end
34 34 @query_count = IssueQuery.visible.count
35 35 @query_pages = Paginator.new @query_count, @limit, params['page']
36 36 @queries = IssueQuery.visible.
37 37 order("#{Query.table_name}.name").
38 38 limit(@limit).
39 39 offset(@offset).
40 40 to_a
41 41 respond_to do |format|
42 42 format.html {render_error :status => 406}
43 43 format.api
44 44 end
45 45 end
46 46
47 47 def new
48 48 @query = IssueQuery.new
49 49 @query.user = User.current
50 50 @query.project = @project
51 51 @query.build_from_params(params)
52 52 end
53 53
54 54 def create
55 55 @query = IssueQuery.new
56 56 @query.user = User.current
57 57 @query.project = @project
58 58 update_query_from_params
59 59
60 60 if @query.save
61 61 flash[:notice] = l(:notice_successful_create)
62 62 redirect_to_issues(:query_id => @query)
63 63 else
64 64 render :action => 'new', :layout => !request.xhr?
65 65 end
66 66 end
67 67
68 68 def edit
69 69 end
70 70
71 71 def update
72 72 update_query_from_params
73 73
74 74 if @query.save
75 75 flash[:notice] = l(:notice_successful_update)
76 76 redirect_to_issues(:query_id => @query)
77 77 else
78 78 render :action => 'edit'
79 79 end
80 80 end
81 81
82 82 def destroy
83 83 @query.destroy
84 84 redirect_to_issues(:set_filter => 1)
85 85 end
86 86
87 87 private
88 88 def find_query
89 89 @query = IssueQuery.find(params[:id])
90 90 @project = @query.project
91 91 render_403 unless @query.editable_by?(User.current)
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def find_optional_project
97 97 @project = Project.find(params[:project_id]) if params[:project_id]
98 98 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
99 99 rescue ActiveRecord::RecordNotFound
100 100 render_404
101 101 end
102 102
103 103 def update_query_from_params
104 104 @query.project = params[:query_is_for_all] ? nil : @project
105 105 @query.build_from_params(params)
106 106 @query.column_names = nil if params[:default_columns]
107 107 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
108 108 @query.name = params[:query] && params[:query][:name]
109 109 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
110 110 @query.visibility = (params[:query] && params[:query][:visibility]) || IssueQuery::VISIBILITY_PRIVATE
111 111 @query.role_ids = params[:query] && params[:query][:role_ids]
112 112 else
113 113 @query.visibility = IssueQuery::VISIBILITY_PRIVATE
114 114 end
115 115 @query
116 116 end
117 117
118 118 def redirect_to_issues(options)
119 119 if params[:gantt]
120 120 if @project
121 121 redirect_to project_gantt_path(@project, options)
122 122 else
123 123 redirect_to issues_gantt_path(options)
124 124 end
125 125 else
126 126 redirect_to _project_issues_path(@project, options)
127 127 end
128 128 end
129 129 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 ReportsController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :authorize, :find_issue_statuses
21 21
22 22 def issue_report
23 23 @trackers = @project.trackers
24 24 @versions = @project.shared_versions.sort
25 25 @priorities = IssuePriority.all.reverse
26 26 @categories = @project.issue_categories
27 27 @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
28 28 @authors = @project.users.sort
29 29 @subprojects = @project.descendants.visible
30 30
31 31 @issues_by_tracker = Issue.by_tracker(@project)
32 32 @issues_by_version = Issue.by_version(@project)
33 33 @issues_by_priority = Issue.by_priority(@project)
34 34 @issues_by_category = Issue.by_category(@project)
35 35 @issues_by_assigned_to = Issue.by_assigned_to(@project)
36 36 @issues_by_author = Issue.by_author(@project)
37 37 @issues_by_subproject = Issue.by_subproject(@project) || []
38 38
39 39 render :template => "reports/issue_report"
40 40 end
41 41
42 42 def issue_report_details
43 43 case params[:detail]
44 44 when "tracker"
45 45 @field = "tracker_id"
46 46 @rows = @project.trackers
47 47 @data = Issue.by_tracker(@project)
48 48 @report_title = l(:field_tracker)
49 49 when "version"
50 50 @field = "fixed_version_id"
51 51 @rows = @project.shared_versions.sort
52 52 @data = Issue.by_version(@project)
53 53 @report_title = l(:field_version)
54 54 when "priority"
55 55 @field = "priority_id"
56 56 @rows = IssuePriority.all.reverse
57 57 @data = Issue.by_priority(@project)
58 58 @report_title = l(:field_priority)
59 59 when "category"
60 60 @field = "category_id"
61 61 @rows = @project.issue_categories
62 62 @data = Issue.by_category(@project)
63 63 @report_title = l(:field_category)
64 64 when "assigned_to"
65 65 @field = "assigned_to_id"
66 66 @rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
67 67 @data = Issue.by_assigned_to(@project)
68 68 @report_title = l(:field_assigned_to)
69 69 when "author"
70 70 @field = "author_id"
71 71 @rows = @project.users.sort
72 72 @data = Issue.by_author(@project)
73 73 @report_title = l(:field_author)
74 74 when "subproject"
75 75 @field = "project_id"
76 76 @rows = @project.descendants.visible
77 77 @data = Issue.by_subproject(@project) || []
78 78 @report_title = l(:field_subproject)
79 79 end
80 80
81 81 respond_to do |format|
82 82 if @field
83 83 format.html {}
84 84 else
85 85 format.html { redirect_to :action => 'issue_report', :id => @project }
86 86 end
87 87 end
88 88 end
89 89
90 90 private
91 91
92 92 def find_issue_statuses
93 93 @statuses = IssueStatus.sorted.to_a
94 94 end
95 95 end
@@ -1,439 +1,439
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :find_project_by_project_id, :only => [:new, :create]
32 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 35 before_filter :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 119 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
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 @content = @repository.cat(@path, @rev)
172 172 (show_error_not_found; return) unless @content
173 173 if is_raw ||
174 174 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
175 175 ! is_entry_text_data?(@content, @path)
176 176 # Force the download
177 177 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
178 178 send_type = Redmine::MimeType.of(@path)
179 179 send_opt[:type] = send_type.to_s if send_type
180 180 send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
181 181 send_data @content, send_opt
182 182 else
183 183 # Prevent empty lines when displaying a file with Windows style eol
184 184 # TODO: UTF-16
185 185 # Is this needs? AttachmentsController reads file simply.
186 186 @content.gsub!("\r\n", "\n")
187 187 @changeset = @repository.find_changeset_by_name(@rev)
188 188 end
189 189 end
190 190 private :entry_and_raw
191 191
192 192 def is_entry_text_data?(ent, path)
193 193 # UTF-16 contains "\x00".
194 194 # It is very strict that file contains less than 30% of ascii symbols
195 195 # in non Western Europe.
196 196 return true if Redmine::MimeType.is_type?('text', path)
197 197 # Ruby 1.8.6 has a bug of integer divisions.
198 198 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
199 199 return false if ent.is_binary_data?
200 200 true
201 201 end
202 202 private :is_entry_text_data?
203 203
204 204 def annotate
205 205 @entry = @repository.entry(@path, @rev)
206 206 (show_error_not_found; return) unless @entry
207 207
208 208 @annotate = @repository.scm.annotate(@path, @rev)
209 209 if @annotate.nil? || @annotate.empty?
210 210 (render_error l(:error_scm_annotate); return)
211 211 end
212 212 ann_buf_size = 0
213 213 @annotate.lines.each do |buf|
214 214 ann_buf_size += buf.size
215 215 end
216 216 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
217 217 (render_error l(:error_scm_annotate_big_text_file); return)
218 218 end
219 219 @changeset = @repository.find_changeset_by_name(@rev)
220 220 end
221 221
222 222 def revision
223 223 respond_to do |format|
224 224 format.html
225 225 format.js {render :layout => false}
226 226 end
227 227 end
228 228
229 229 # Adds a related issue to a changeset
230 230 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
231 231 def add_related_issue
232 232 issue_id = params[:issue_id].to_s.sub(/^#/,'')
233 233 @issue = @changeset.find_referenced_issue_by_id(issue_id)
234 234 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
235 235 @issue = nil
236 236 end
237 237
238 238 if @issue
239 239 @changeset.issues << @issue
240 240 end
241 241 end
242 242
243 243 # Removes a related issue from a changeset
244 244 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
245 245 def remove_related_issue
246 246 @issue = Issue.visible.find_by_id(params[:issue_id])
247 247 if @issue
248 248 @changeset.issues.delete(@issue)
249 249 end
250 250 end
251 251
252 252 def diff
253 253 if params[:format] == 'diff'
254 254 @diff = @repository.diff(@path, @rev, @rev_to)
255 255 (show_error_not_found; return) unless @diff
256 256 filename = "changeset_r#{@rev}"
257 257 filename << "_r#{@rev_to}" if @rev_to
258 258 send_data @diff.join, :filename => "#{filename}.diff",
259 259 :type => 'text/x-patch',
260 260 :disposition => 'attachment'
261 261 else
262 262 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
263 263 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
264 264
265 265 # Save diff type as user preference
266 266 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
267 267 User.current.pref[:diff_type] = @diff_type
268 268 User.current.preference.save
269 269 end
270 270 @cache_key = "repositories/diff/#{@repository.id}/" +
271 271 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
272 272 unless read_fragment(@cache_key)
273 273 @diff = @repository.diff(@path, @rev, @rev_to)
274 274 show_error_not_found unless @diff
275 275 end
276 276
277 277 @changeset = @repository.find_changeset_by_name(@rev)
278 278 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
279 279 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
280 280 end
281 281 end
282 282
283 283 def stats
284 284 end
285 285
286 286 def graph
287 287 data = nil
288 288 case params[:graph]
289 289 when "commits_per_month"
290 290 data = graph_commits_per_month(@repository)
291 291 when "commits_per_author"
292 292 data = graph_commits_per_author(@repository)
293 293 end
294 294 if data
295 295 headers["Content-Type"] = "image/svg+xml"
296 296 send_data(data, :type => "image/svg+xml", :disposition => "inline")
297 297 else
298 298 render_404
299 299 end
300 300 end
301 301
302 302 private
303 303
304 304 def find_repository
305 305 @repository = Repository.find(params[:id])
306 306 @project = @repository.project
307 307 rescue ActiveRecord::RecordNotFound
308 308 render_404
309 309 end
310 310
311 311 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
312 312
313 313 def find_project_repository
314 314 @project = Project.find(params[:id])
315 315 if params[:repository_id].present?
316 316 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
317 317 else
318 318 @repository = @project.repository
319 319 end
320 320 (render_404; return false) unless @repository
321 321 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
322 322 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
323 323 @rev_to = params[:rev_to]
324 324
325 325 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
326 326 if @repository.branches.blank?
327 327 raise InvalidRevisionParam
328 328 end
329 329 end
330 330 rescue ActiveRecord::RecordNotFound
331 331 render_404
332 332 rescue InvalidRevisionParam
333 333 show_error_not_found
334 334 end
335 335
336 336 def find_changeset
337 337 if @rev.present?
338 338 @changeset = @repository.find_changeset_by_name(@rev)
339 339 end
340 340 show_error_not_found unless @changeset
341 341 end
342 342
343 343 def show_error_not_found
344 344 render_error :message => l(:error_scm_not_found), :status => 404
345 345 end
346 346
347 347 # Handler for Redmine::Scm::Adapters::CommandFailed exception
348 348 def show_error_command_failed(exception)
349 349 render_error l(:error_scm_command_failed, exception.message)
350 350 end
351 351
352 352 def graph_commits_per_month(repository)
353 353 @date_to = Date.today
354 354 @date_from = @date_to << 11
355 355 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
356 356 commits_by_day = Changeset.
357 357 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
358 358 group(:commit_date).
359 359 count
360 360 commits_by_month = [0] * 12
361 361 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
362 362
363 363 changes_by_day = Change.
364 364 joins(:changeset).
365 365 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
366 366 group(:commit_date).
367 367 count
368 368 changes_by_month = [0] * 12
369 369 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
370 370
371 371 fields = []
372 372 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
373 373
374 374 graph = SVG::Graph::Bar.new(
375 375 :height => 300,
376 376 :width => 800,
377 377 :fields => fields.reverse,
378 378 :stack => :side,
379 379 :scale_integers => true,
380 380 :step_x_labels => 2,
381 381 :show_data_values => false,
382 382 :graph_title => l(:label_commits_per_month),
383 383 :show_graph_title => true
384 384 )
385 385
386 386 graph.add_data(
387 387 :data => commits_by_month[0..11].reverse,
388 388 :title => l(:label_revision_plural)
389 389 )
390 390
391 391 graph.add_data(
392 392 :data => changes_by_month[0..11].reverse,
393 393 :title => l(:label_change_plural)
394 394 )
395 395
396 396 graph.burn
397 397 end
398 398
399 399 def graph_commits_per_author(repository)
400 400 #data
401 401 stats = repository.stats_by_author
402 402 fields, commits_data, changes_data = [], [], []
403 403 stats.each do |name, hsh|
404 404 fields << name
405 405 commits_data << hsh[:commits_count]
406 406 changes_data << hsh[:changes_count]
407 407 end
408 408
409 409 #expand to 10 values if needed
410 410 fields = fields + [""]*(10 - fields.length) if fields.length<10
411 411 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
412 412 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
413 413
414 414 # Remove email address in usernames
415 415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
416 416
417 417 #prepare graph
418 418 graph = SVG::Graph::BarHorizontal.new(
419 419 :height => 30 * commits_data.length,
420 420 :width => 800,
421 421 :fields => fields,
422 422 :stack => :side,
423 423 :scale_integers => true,
424 424 :show_data_values => false,
425 425 :rotate_y_labels => false,
426 426 :graph_title => l(:label_commits_per_author),
427 427 :show_graph_title => true
428 428 )
429 429 graph.add_data(
430 430 :data => commits_data,
431 431 :title => l(:label_revision_plural)
432 432 )
433 433 graph.add_data(
434 434 :data => changes_data,
435 435 :title => l(:label_change_plural)
436 436 )
437 437 graph.burn
438 438 end
439 439 end
@@ -1,110 +1,110
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_admin, :except => [:index, :show]
22 22 before_filter :require_admin_or_api_request, :only => [:index, :show]
23 23 before_filter :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 @role_pages, @roles = paginate Role.sorted, :per_page => 25
32 32 render :action => "index", :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(params[:role] || {:permissions => Role.non_member.permissions})
49 49 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
50 50 @role.copy_from(@copy_from)
51 51 end
52 52 @roles = Role.sorted.to_a
53 53 end
54 54
55 55 def create
56 56 @role = Role.new(params[:role])
57 57 if request.post? && @role.save
58 58 # workflow copy
59 59 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
60 60 @role.workflow_rules.copy(copy_from)
61 61 end
62 62 flash[:notice] = l(:notice_successful_create)
63 63 redirect_to roles_path
64 64 else
65 65 @roles = Role.sorted.to_a
66 66 render :action => 'new'
67 67 end
68 68 end
69 69
70 70 def edit
71 71 end
72 72
73 73 def update
74 74 if @role.update_attributes(params[:role])
75 75 flash[:notice] = l(:notice_successful_update)
76 76 redirect_to roles_path(:page => params[:page])
77 77 else
78 78 render :action => 'edit'
79 79 end
80 80 end
81 81
82 82 def destroy
83 83 @role.destroy
84 84 redirect_to roles_path
85 85 rescue
86 86 flash[:error] = l(:error_can_not_remove_role)
87 87 redirect_to roles_path
88 88 end
89 89
90 90 def permissions
91 91 @roles = Role.sorted.to_a
92 92 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
93 93 if request.post?
94 94 @roles.each do |role|
95 95 role.permissions = params[:permissions][role.id.to_s]
96 96 role.save
97 97 end
98 98 flash[:notice] = l(:notice_successful_update)
99 99 redirect_to roles_path
100 100 end
101 101 end
102 102
103 103 private
104 104
105 105 def find_role
106 106 @role = Role.find(params[:id])
107 107 rescue ActiveRecord::RecordNotFound
108 108 render_404
109 109 end
110 110 end
@@ -1,87 +1,87
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 21 def index
22 22 @question = params[:q] || ""
23 23 @question.strip!
24 24 @all_words = params[:all_words] ? params[:all_words].present? : true
25 25 @titles_only = params[:titles_only] ? params[:titles_only].present? : false
26 26 @search_attachments = params[:attachments].presence || '0'
27 27 @open_issues = params[:open_issues] ? params[:open_issues].present? : false
28 28
29 29 # quick jump to an issue
30 30 if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
31 31 redirect_to issue_path(issue)
32 32 return
33 33 end
34 34
35 35 projects_to_search =
36 36 case params[:scope]
37 37 when 'all'
38 38 nil
39 39 when 'my_projects'
40 40 User.current.projects
41 41 when 'subprojects'
42 42 @project ? (@project.self_and_descendants.active.to_a) : nil
43 43 else
44 44 @project
45 45 end
46 46
47 47 @object_types = Redmine::Search.available_search_types.dup
48 48 if projects_to_search.is_a? Project
49 49 # don't search projects
50 50 @object_types.delete('projects')
51 51 # only show what the user is allowed to view
52 52 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
53 53 end
54 54
55 55 @scope = @object_types.select {|t| params[t]}
56 56 @scope = @object_types if @scope.empty?
57 57
58 58 fetcher = Redmine::Search::Fetcher.new(
59 59 @question, User.current, @scope, projects_to_search,
60 60 :all_words => @all_words, :titles_only => @titles_only, :attachments => @search_attachments, :open_issues => @open_issues,
61 61 :cache => params[:page].present?
62 62 )
63 63
64 64 if fetcher.tokens.present?
65 65 @result_count = fetcher.result_count
66 66 @result_count_by_type = fetcher.result_count_by_type
67 67 @tokens = fetcher.tokens
68 68
69 69 per_page = Setting.search_results_per_page.to_i
70 70 per_page = 10 if per_page == 0
71 71 @result_pages = Paginator.new @result_count, per_page, params['page']
72 72 @results = fetcher.results(@result_pages.offset, @result_pages.per_page)
73 73 else
74 74 @question = ""
75 75 end
76 76 render :layout => false if request.xhr?
77 77 end
78 78
79 79 private
80 80 def find_optional_project
81 81 return true unless params[:id]
82 82 @project = Project.find(params[:id])
83 83 check_project_privacy
84 84 rescue ActiveRecord::RecordNotFound
85 85 render_404
86 86 end
87 87 end
@@ -1,76 +1,76
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 SettingsController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :plugins, :only => :plugin
21 21
22 22 helper :queries
23 23
24 24 before_filter :require_admin
25 25
26 26 require_sudo_mode :index, :edit, :plugin
27 27
28 28 def index
29 29 edit
30 30 render :action => 'edit'
31 31 end
32 32
33 33 def edit
34 34 @notifiables = Redmine::Notifiable.all
35 35 if request.post? && params[:settings] && params[:settings].is_a?(Hash)
36 36 settings = (params[:settings] || {}).dup.symbolize_keys
37 37 settings.each do |name, value|
38 38 Setting.set_from_params name, value
39 39 end
40 40 flash[:notice] = l(:notice_successful_update)
41 41 redirect_to settings_path(:tab => params[:tab])
42 42 else
43 43 @options = {}
44 44 user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
45 45 @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
46 46 @deliveries = ActionMailer::Base.perform_deliveries
47 47
48 48 @guessed_host_and_path = request.host_with_port.dup
49 49 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
50 50
51 51 @commit_update_keywords = Setting.commit_update_keywords.dup
52 52 @commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?
53 53
54 54 Redmine::Themes.rescan
55 55 end
56 56 end
57 57
58 58 def plugin
59 59 @plugin = Redmine::Plugin.find(params[:id])
60 60 unless @plugin.configurable?
61 61 render_404
62 62 return
63 63 end
64 64
65 65 if request.post?
66 66 Setting.send "plugin_#{@plugin.id}=", params[:settings]
67 67 flash[:notice] = l(:notice_successful_update)
68 68 redirect_to plugin_settings_path(@plugin)
69 69 else
70 70 @partial = @plugin.settings[:partial]
71 71 @settings = Setting.send "plugin_#{@plugin.id}"
72 72 end
73 73 rescue Redmine::PluginNotFound
74 74 render_404
75 75 end
76 76 end
@@ -1,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :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 34 render :nothing => true, :status => 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 42 render :nothing => true, :status => 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 67 render :nothing => true, :status => 200
68 68 rescue ActiveRecord::RecordNotFound
69 69 render :nothing => true, :status => 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,281 +1,281
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24 24
25 25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
26 26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27 27
28 28 accept_rss_auth :index
29 29 accept_api_auth :index, :show, :create, :update, :destroy
30 30
31 31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 32
33 33 helper :sort
34 34 include SortHelper
35 35 helper :issues
36 36 include TimelogHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :queries
40 40 include QueriesHelper
41 41
42 42 def index
43 43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
44 44
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 @total_hours = scope.sum(:hours).to_f
57 57
58 58 render :layout => !request.xhr?
59 59 }
60 60 format.api {
61 61 @entry_count = scope.count
62 62 @offset, @limit = api_offset_and_limit
63 63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
64 64 }
65 65 format.atom {
66 66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
67 67 render_feed(entries, :title => l(:label_spent_time))
68 68 }
69 69 format.csv {
70 70 # Export all entries
71 71 @entries = scope.to_a
72 72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
73 73 }
74 74 end
75 75 end
76 76
77 77 def report
78 78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
79 79 scope = time_entry_scope
80 80
81 81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
82 82
83 83 respond_to do |format|
84 84 format.html { render :layout => !request.xhr? }
85 85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
86 86 end
87 87 end
88 88
89 89 def show
90 90 respond_to do |format|
91 91 # TODO: Implement html response
92 92 format.html { render :nothing => true, :status => 406 }
93 93 format.api
94 94 end
95 95 end
96 96
97 97 def new
98 98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
99 99 @time_entry.safe_attributes = params[:time_entry]
100 100 end
101 101
102 102 def create
103 103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
104 104 @time_entry.safe_attributes = params[:time_entry]
105 105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 106 render_403
107 107 return
108 108 end
109 109
110 110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
111 111
112 112 if @time_entry.save
113 113 respond_to do |format|
114 114 format.html {
115 115 flash[:notice] = l(:notice_successful_create)
116 116 if params[:continue]
117 117 options = {
118 118 :time_entry => {
119 119 :project_id => params[:time_entry][:project_id],
120 120 :issue_id => @time_entry.issue_id,
121 121 :activity_id => @time_entry.activity_id
122 122 },
123 123 :back_url => params[:back_url]
124 124 }
125 125 if params[:project_id] && @time_entry.project
126 126 redirect_to new_project_time_entry_path(@time_entry.project, options)
127 127 elsif params[:issue_id] && @time_entry.issue
128 128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
129 129 else
130 130 redirect_to new_time_entry_path(options)
131 131 end
132 132 else
133 133 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 134 end
135 135 }
136 136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 137 end
138 138 else
139 139 respond_to do |format|
140 140 format.html { render :action => 'new' }
141 141 format.api { render_validation_errors(@time_entry) }
142 142 end
143 143 end
144 144 end
145 145
146 146 def edit
147 147 @time_entry.safe_attributes = params[:time_entry]
148 148 end
149 149
150 150 def update
151 151 @time_entry.safe_attributes = params[:time_entry]
152 152
153 153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154 154
155 155 if @time_entry.save
156 156 respond_to do |format|
157 157 format.html {
158 158 flash[:notice] = l(:notice_successful_update)
159 159 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 160 }
161 161 format.api { render_api_ok }
162 162 end
163 163 else
164 164 respond_to do |format|
165 165 format.html { render :action => 'edit' }
166 166 format.api { render_validation_errors(@time_entry) }
167 167 end
168 168 end
169 169 end
170 170
171 171 def bulk_edit
172 172 @available_activities = TimeEntryActivity.shared.active
173 173 @custom_fields = TimeEntry.first.available_custom_fields
174 174 end
175 175
176 176 def bulk_update
177 177 attributes = parse_params_for_bulk_time_entry_attributes(params)
178 178
179 179 unsaved_time_entry_ids = []
180 180 @time_entries.each do |time_entry|
181 181 time_entry.reload
182 182 time_entry.safe_attributes = attributes
183 183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 184 unless time_entry.save
185 185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
186 186 # Keep unsaved time_entry ids to display them in flash error
187 187 unsaved_time_entry_ids << time_entry.id
188 188 end
189 189 end
190 190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 191 redirect_back_or_default project_time_entries_path(@projects.first)
192 192 end
193 193
194 194 def destroy
195 195 destroyed = TimeEntry.transaction do
196 196 @time_entries.each do |t|
197 197 unless t.destroy && t.destroyed?
198 198 raise ActiveRecord::Rollback
199 199 end
200 200 end
201 201 end
202 202
203 203 respond_to do |format|
204 204 format.html {
205 205 if destroyed
206 206 flash[:notice] = l(:notice_successful_delete)
207 207 else
208 208 flash[:error] = l(:notice_unable_delete_time_entry)
209 209 end
210 210 redirect_back_or_default project_time_entries_path(@projects.first)
211 211 }
212 212 format.api {
213 213 if destroyed
214 214 render_api_ok
215 215 else
216 216 render_validation_errors(@time_entries)
217 217 end
218 218 }
219 219 end
220 220 end
221 221
222 222 private
223 223 def find_time_entry
224 224 @time_entry = TimeEntry.find(params[:id])
225 225 unless @time_entry.editable_by?(User.current)
226 226 render_403
227 227 return false
228 228 end
229 229 @project = @time_entry.project
230 230 rescue ActiveRecord::RecordNotFound
231 231 render_404
232 232 end
233 233
234 234 def find_time_entries
235 235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
236 236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 237 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
238 238 @projects = @time_entries.collect(&:project).compact.uniq
239 239 @project = @projects.first if @projects.size == 1
240 240 rescue ActiveRecord::RecordNotFound
241 241 render_404
242 242 end
243 243
244 244 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
245 245 if unsaved_time_entry_ids.empty?
246 246 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
247 247 else
248 248 flash[:error] = l(:notice_failed_to_save_time_entries,
249 249 :count => unsaved_time_entry_ids.size,
250 250 :total => time_entries.size,
251 251 :ids => '#' + unsaved_time_entry_ids.join(', #'))
252 252 end
253 253 end
254 254
255 255 def find_optional_project
256 256 if params[:issue_id].present?
257 257 @issue = Issue.find(params[:issue_id])
258 258 @project = @issue.project
259 259 elsif params[:project_id].present?
260 260 @project = Project.find(params[:project_id])
261 261 end
262 262 rescue ActiveRecord::RecordNotFound
263 263 render_404
264 264 end
265 265
266 266 # Returns the TimeEntry scope for index and report actions
267 267 def time_entry_scope(options={})
268 268 scope = @query.results_scope(options)
269 269 if @issue
270 270 scope = scope.on_issue(@issue)
271 271 end
272 272 scope
273 273 end
274 274
275 275 def parse_params_for_bulk_time_entry_attributes(params)
276 276 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
277 277 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
278 278 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
279 279 attributes
280 280 end
281 281 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @tracker_pages, @trackers = paginate Tracker.sorted, :per_page => 25
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @trackers = Tracker.sorted.to_a
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @tracker ||= Tracker.new(params[:tracker])
39 39 @trackers = Tracker.sorted.to_a
40 40 @projects = Project.all
41 41 end
42 42
43 43 def create
44 44 @tracker = Tracker.new(params[:tracker])
45 45 if @tracker.save
46 46 # workflow copy
47 47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 48 @tracker.workflow_rules.copy(copy_from)
49 49 end
50 50 flash[:notice] = l(:notice_successful_create)
51 51 redirect_to trackers_path
52 52 return
53 53 end
54 54 new
55 55 render :action => 'new'
56 56 end
57 57
58 58 def edit
59 59 @tracker ||= Tracker.find(params[:id])
60 60 @projects = Project.all
61 61 end
62 62
63 63 def update
64 64 @tracker = Tracker.find(params[:id])
65 65 if @tracker.update_attributes(params[:tracker])
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to trackers_path(:page => params[:page])
68 68 return
69 69 end
70 70 edit
71 71 render :action => 'edit'
72 72 end
73 73
74 74 def destroy
75 75 @tracker = Tracker.find(params[:id])
76 76 unless @tracker.issues.empty?
77 77 flash[:error] = l(:error_can_not_delete_tracker)
78 78 else
79 79 @tracker.destroy
80 80 end
81 81 redirect_to trackers_path
82 82 end
83 83
84 84 def fields
85 85 if request.post? && params[:trackers]
86 86 params[:trackers].each do |tracker_id, tracker_params|
87 87 tracker = Tracker.find_by_id(tracker_id)
88 88 if tracker
89 89 tracker.core_fields = tracker_params[:core_fields]
90 90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
91 91 tracker.save
92 92 end
93 93 end
94 94 flash[:notice] = l(:notice_successful_update)
95 95 redirect_to fields_trackers_path
96 96 return
97 97 end
98 98 @trackers = Tracker.sorted.to_a
99 99 @custom_fields = IssueCustomField.all.sort
100 100 end
101 101 end
@@ -1,190 +1,190
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy]
23 23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29 helper :principal_memberships
30 30
31 31 require_sudo_mode :create, :update, :destroy
32 32
33 33 def index
34 34 sort_init 'login', 'asc'
35 35 sort_update %w(login firstname lastname admin created_on last_login_on)
36 36
37 37 case params[:format]
38 38 when 'xml', 'json'
39 39 @offset, @limit = api_offset_and_limit
40 40 else
41 41 @limit = per_page_option
42 42 end
43 43
44 44 @status = params[:status] || 1
45 45
46 46 scope = User.logged.status(@status).preload(:email_address)
47 47 scope = scope.like(params[:name]) if params[:name].present?
48 48 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
49 49
50 50 @user_count = scope.count
51 51 @user_pages = Paginator.new @user_count, @limit, params['page']
52 52 @offset ||= @user_pages.offset
53 53 @users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a
54 54
55 55 respond_to do |format|
56 56 format.html {
57 57 @groups = Group.all.sort
58 58 render :layout => !request.xhr?
59 59 }
60 60 format.api
61 61 end
62 62 end
63 63
64 64 def show
65 65 unless @user.visible?
66 66 render_404
67 67 return
68 68 end
69 69
70 70 # show projects based on current user visibility
71 71 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
72 72
73 73 respond_to do |format|
74 74 format.html {
75 75 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
76 76 @events_by_day = events.group_by(&:event_date)
77 77 render :layout => 'base'
78 78 }
79 79 format.api
80 80 end
81 81 end
82 82
83 83 def new
84 84 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
85 85 @user.safe_attributes = params[:user]
86 86 @auth_sources = AuthSource.all
87 87 end
88 88
89 89 def create
90 90 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
91 91 @user.safe_attributes = params[:user]
92 92 @user.admin = params[:user][:admin] || false
93 93 @user.login = params[:user][:login]
94 94 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
95 95 @user.pref.attributes = params[:pref] if params[:pref]
96 96
97 97 if @user.save
98 98 Mailer.account_information(@user, @user.password).deliver if params[:send_information]
99 99
100 100 respond_to do |format|
101 101 format.html {
102 102 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
103 103 if params[:continue]
104 104 attrs = params[:user].slice(:generate_password)
105 105 redirect_to new_user_path(:user => attrs)
106 106 else
107 107 redirect_to edit_user_path(@user)
108 108 end
109 109 }
110 110 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
111 111 end
112 112 else
113 113 @auth_sources = AuthSource.all
114 114 # Clear password input
115 115 @user.password = @user.password_confirmation = nil
116 116
117 117 respond_to do |format|
118 118 format.html { render :action => 'new' }
119 119 format.api { render_validation_errors(@user) }
120 120 end
121 121 end
122 122 end
123 123
124 124 def edit
125 125 @auth_sources = AuthSource.all
126 126 @membership ||= Member.new
127 127 end
128 128
129 129 def update
130 130 @user.admin = params[:user][:admin] if params[:user][:admin]
131 131 @user.login = params[:user][:login] if params[:user][:login]
132 132 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
133 133 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
134 134 end
135 135 @user.safe_attributes = params[:user]
136 136 # Was the account actived ? (do it before User#save clears the change)
137 137 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
138 138 # TODO: Similar to My#account
139 139 @user.pref.attributes = params[:pref] if params[:pref]
140 140
141 141 if @user.save
142 142 @user.pref.save
143 143
144 144 if was_activated
145 145 Mailer.account_activated(@user).deliver
146 146 elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil? && @user != User.current
147 147 Mailer.account_information(@user, @user.password).deliver
148 148 end
149 149
150 150 respond_to do |format|
151 151 format.html {
152 152 flash[:notice] = l(:notice_successful_update)
153 153 redirect_to_referer_or edit_user_path(@user)
154 154 }
155 155 format.api { render_api_ok }
156 156 end
157 157 else
158 158 @auth_sources = AuthSource.all
159 159 @membership ||= Member.new
160 160 # Clear password input
161 161 @user.password = @user.password_confirmation = nil
162 162
163 163 respond_to do |format|
164 164 format.html { render :action => :edit }
165 165 format.api { render_validation_errors(@user) }
166 166 end
167 167 end
168 168 end
169 169
170 170 def destroy
171 171 @user.destroy
172 172 respond_to do |format|
173 173 format.html { redirect_back_or_default(users_path) }
174 174 format.api { render_api_ok }
175 175 end
176 176 end
177 177
178 178 private
179 179
180 180 def find_user
181 181 if params[:id] == 'current'
182 182 require_login || return
183 183 @user = User.current
184 184 else
185 185 @user = User.find(params[:id])
186 186 end
187 187 rescue ActiveRecord::RecordNotFound
188 188 render_404
189 189 end
190 190 end
@@ -1,182 +1,182
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 model_object Version
21 21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
24 24 before_filter :authorize
25 25
26 26 accept_api_auth :index, :show, :create, :update, :destroy
27 27
28 28 helper :custom_fields
29 29 helper :projects
30 30
31 31 def index
32 32 respond_to do |format|
33 33 format.html {
34 34 @trackers = @project.trackers.sorted.to_a
35 35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38 38
39 39 @versions = @project.shared_versions || []
40 40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 41 @versions = @versions.uniq.sort
42 42 unless params[:completed]
43 43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 44 @versions -= @completed_versions
45 45 end
46 46
47 47 @issues_by_version = {}
48 48 if @selected_tracker_ids.any? && @versions.any?
49 49 issues = Issue.visible.
50 50 includes(:project, :tracker).
51 51 preload(:status, :priority, :fixed_version).
52 52 where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
53 53 order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
54 54 @issues_by_version = issues.group_by(&:fixed_version)
55 55 end
56 56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 57 }
58 58 format.api {
59 59 @versions = @project.shared_versions.to_a
60 60 }
61 61 end
62 62 end
63 63
64 64 def show
65 65 respond_to do |format|
66 66 format.html {
67 67 @issues = @version.fixed_issues.visible.
68 68 includes(:status, :tracker, :priority).
69 69 reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
70 70 to_a
71 71 }
72 72 format.api
73 73 end
74 74 end
75 75
76 76 def new
77 77 @version = @project.versions.build
78 78 @version.safe_attributes = params[:version]
79 79
80 80 respond_to do |format|
81 81 format.html
82 82 format.js
83 83 end
84 84 end
85 85
86 86 def create
87 87 @version = @project.versions.build
88 88 if params[:version]
89 89 attributes = params[:version].dup
90 90 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
91 91 @version.safe_attributes = attributes
92 92 end
93 93
94 94 if request.post?
95 95 if @version.save
96 96 respond_to do |format|
97 97 format.html do
98 98 flash[:notice] = l(:notice_successful_create)
99 99 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
100 100 end
101 101 format.js
102 102 format.api do
103 103 render :action => 'show', :status => :created, :location => version_url(@version)
104 104 end
105 105 end
106 106 else
107 107 respond_to do |format|
108 108 format.html { render :action => 'new' }
109 109 format.js { render :action => 'new' }
110 110 format.api { render_validation_errors(@version) }
111 111 end
112 112 end
113 113 end
114 114 end
115 115
116 116 def edit
117 117 end
118 118
119 119 def update
120 120 if params[:version]
121 121 attributes = params[:version].dup
122 122 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
123 123 @version.safe_attributes = attributes
124 124 if @version.save
125 125 respond_to do |format|
126 126 format.html {
127 127 flash[:notice] = l(:notice_successful_update)
128 128 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
129 129 }
130 130 format.api { render_api_ok }
131 131 end
132 132 else
133 133 respond_to do |format|
134 134 format.html { render :action => 'edit' }
135 135 format.api { render_validation_errors(@version) }
136 136 end
137 137 end
138 138 end
139 139 end
140 140
141 141 def close_completed
142 142 if request.put?
143 143 @project.close_completed_versions
144 144 end
145 145 redirect_to settings_project_path(@project, :tab => 'versions')
146 146 end
147 147
148 148 def destroy
149 149 if @version.deletable?
150 150 @version.destroy
151 151 respond_to do |format|
152 152 format.html { redirect_back_or_default settings_project_path(@project, :tab => 'versions') }
153 153 format.api { render_api_ok }
154 154 end
155 155 else
156 156 respond_to do |format|
157 157 format.html {
158 158 flash[:error] = l(:notice_unable_delete_version)
159 159 redirect_to settings_project_path(@project, :tab => 'versions')
160 160 }
161 161 format.api { head :unprocessable_entity }
162 162 end
163 163 end
164 164 end
165 165
166 166 def status_by
167 167 respond_to do |format|
168 168 format.html { render :action => 'show' }
169 169 format.js
170 170 end
171 171 end
172 172
173 173 private
174 174
175 175 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
176 176 if ids = params[:tracker_ids]
177 177 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
178 178 else
179 179 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
180 180 end
181 181 end
182 182 end
@@ -1,133 +1,133
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 WatchersController < ApplicationController
19 19 before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
20 20
21 21 def watch
22 22 set_watcher(@watchables, User.current, true)
23 23 end
24 24
25 25 def unwatch
26 26 set_watcher(@watchables, User.current, false)
27 27 end
28 28
29 29 before_filter :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
30 30 accept_api_auth :create, :destroy
31 31
32 32 def new
33 33 @users = users_for_new_watcher
34 34 end
35 35
36 36 def create
37 37 user_ids = []
38 38 if params[:watcher].is_a?(Hash)
39 39 user_ids << (params[:watcher][:user_ids] || params[:watcher][:user_id])
40 40 else
41 41 user_ids << params[:user_id]
42 42 end
43 43 users = User.active.visible.where(:id => user_ids.flatten.compact.uniq)
44 44 users.each do |user|
45 45 Watcher.create(:watchable => @watched, :user => user)
46 46 end
47 47 respond_to do |format|
48 48 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
49 49 format.js { @users = users_for_new_watcher }
50 50 format.api { render_api_ok }
51 51 end
52 52 end
53 53
54 54 def append
55 55 if params[:watcher].is_a?(Hash)
56 56 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
57 57 @users = User.active.visible.where(:id => user_ids).to_a
58 58 end
59 59 if @users.blank?
60 60 render :nothing => true
61 61 end
62 62 end
63 63
64 64 def destroy
65 65 @watched.set_watcher(User.find(params[:user_id]), false)
66 66 respond_to do |format|
67 67 format.html { redirect_to :back }
68 68 format.js
69 69 format.api { render_api_ok }
70 70 end
71 71 rescue ActiveRecord::RecordNotFound
72 72 render_404
73 73 end
74 74
75 75 def autocomplete_for_user
76 76 @users = users_for_new_watcher
77 77 render :layout => false
78 78 end
79 79
80 80 private
81 81
82 82 def find_project
83 83 if params[:object_type] && params[:object_id]
84 84 klass = Object.const_get(params[:object_type].camelcase)
85 85 return false unless klass.respond_to?('watched_by')
86 86 @watched = klass.find(params[:object_id])
87 87 @project = @watched.project
88 88 elsif params[:project_id]
89 89 @project = Project.visible.find_by_param(params[:project_id])
90 90 end
91 91 rescue
92 92 render_404
93 93 end
94 94
95 95 def find_watchables
96 96 klass = Object.const_get(params[:object_type].camelcase) rescue nil
97 97 if klass && klass.respond_to?('watched_by')
98 98 @watchables = klass.where(:id => Array.wrap(params[:object_id])).to_a
99 99 raise Unauthorized if @watchables.any? {|w|
100 100 if w.respond_to?(:visible?)
101 101 !w.visible?
102 102 elsif w.respond_to?(:project) && w.project
103 103 !w.project.visible?
104 104 end
105 105 }
106 106 end
107 107 render_404 unless @watchables.present?
108 108 end
109 109
110 110 def set_watcher(watchables, user, watching)
111 111 watchables.each do |watchable|
112 112 watchable.set_watcher(user, watching)
113 113 end
114 114 respond_to do |format|
115 115 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
116 116 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
117 117 end
118 118 end
119 119
120 120 def users_for_new_watcher
121 121 scope = nil
122 122 if params[:q].blank? && @project.present?
123 123 scope = @project.users
124 124 else
125 125 scope = User.all.limit(100)
126 126 end
127 127 users = scope.active.visible.sorted.like(params[:q]).to_a
128 128 if @watched
129 129 users -= @watched.watcher_users
130 130 end
131 131 users
132 132 end
133 133 end
@@ -1,29 +1,29
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 WelcomeController < ApplicationController
19 19 caches_action :robots
20 20
21 21 def index
22 22 @news = News.latest User.current
23 23 end
24 24
25 25 def robots
26 26 @projects = Project.all_public.active
27 27 render :layout => false, :content_type => 'text/plain'
28 28 end
29 29 end
@@ -1,370 +1,370
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 # The WikiController follows the Rails REST controller pattern but with
19 19 # a few differences
20 20 #
21 21 # * index - shows a list of WikiPages grouped by page or date
22 22 # * new - not used
23 23 # * create - not used
24 24 # * show - will also show the form for creating a new wiki page
25 25 # * edit - used to edit an existing or new page
26 26 # * update - used to save a wiki page update to the database, including new pages
27 27 # * destroy - normal
28 28 #
29 29 # Other member and collection methods are also used
30 30 #
31 31 # TODO: still being worked on
32 32 class WikiController < ApplicationController
33 33 default_search_scope :wiki_pages
34 34 before_filter :find_wiki, :authorize
35 35 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
36 36 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
37 37 accept_api_auth :index, :show, :update, :destroy
38 38 before_filter :find_attachments, :only => [:preview]
39 39
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :watchers
43 43 include Redmine::Export::PDF
44 44
45 45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 46 def index
47 47 load_pages_for_index
48 48
49 49 respond_to do |format|
50 50 format.html {
51 51 @pages_by_parent_id = @pages.group_by(&:parent_id)
52 52 }
53 53 format.api
54 54 end
55 55 end
56 56
57 57 # List of page, by last update
58 58 def date_index
59 59 load_pages_for_index
60 60 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
61 61 end
62 62
63 63 # display a page (in editing mode if it doesn't exist)
64 64 def show
65 65 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
66 66 deny_access
67 67 return
68 68 end
69 69 @content = @page.content_for_version(params[:version])
70 70 if @content.nil?
71 71 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
72 72 edit
73 73 render :action => 'edit'
74 74 else
75 75 render_404
76 76 end
77 77 return
78 78 end
79 79 if User.current.allowed_to?(:export_wiki_pages, @project)
80 80 if params[:format] == 'pdf'
81 81 send_file_headers! :type => 'application/pdf', :filename => "#{@page.title}.pdf"
82 82 return
83 83 elsif params[:format] == 'html'
84 84 export = render_to_string :action => 'export', :layout => false
85 85 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
86 86 return
87 87 elsif params[:format] == 'txt'
88 88 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
89 89 return
90 90 end
91 91 end
92 92 @editable = editable?
93 93 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
94 94 @content.current_version? &&
95 95 Redmine::WikiFormatting.supports_section_edit?
96 96
97 97 respond_to do |format|
98 98 format.html
99 99 format.api
100 100 end
101 101 end
102 102
103 103 # edit an existing page or a new one
104 104 def edit
105 105 return render_403 unless editable?
106 106 if @page.new_record?
107 107 if params[:parent].present?
108 108 @page.parent = @page.wiki.find_page(params[:parent].to_s)
109 109 end
110 110 end
111 111
112 112 @content = @page.content_for_version(params[:version])
113 113 @content ||= WikiContent.new(:page => @page)
114 114 @content.text = initial_page_content(@page) if @content.text.blank?
115 115 # don't keep previous comment
116 116 @content.comments = nil
117 117
118 118 # To prevent StaleObjectError exception when reverting to a previous version
119 119 @content.version = @page.content.version if @page.content
120 120
121 121 @text = @content.text
122 122 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
123 123 @section = params[:section].to_i
124 124 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
125 125 render_404 if @text.blank?
126 126 end
127 127 end
128 128
129 129 # Creates a new page or updates an existing one
130 130 def update
131 131 return render_403 unless editable?
132 132 was_new_page = @page.new_record?
133 133 @page.safe_attributes = params[:wiki_page]
134 134
135 135 @content = @page.content || WikiContent.new(:page => @page)
136 136 content_params = params[:content]
137 137 if content_params.nil? && params[:wiki_page].is_a?(Hash)
138 138 content_params = params[:wiki_page].slice(:text, :comments, :version)
139 139 end
140 140 content_params ||= {}
141 141
142 142 @content.comments = content_params[:comments]
143 143 @text = content_params[:text]
144 144 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
145 145 @section = params[:section].to_i
146 146 @section_hash = params[:section_hash]
147 147 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(@section, @text, @section_hash)
148 148 else
149 149 @content.version = content_params[:version] if content_params[:version]
150 150 @content.text = @text
151 151 end
152 152 @content.author = User.current
153 153
154 154 if @page.save_with_content(@content)
155 155 attachments = Attachment.attach_files(@page, params[:attachments])
156 156 render_attachment_warning_if_needed(@page)
157 157 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
158 158
159 159 respond_to do |format|
160 160 format.html {
161 161 anchor = @section ? "section-#{@section}" : nil
162 162 redirect_to project_wiki_page_path(@project, @page.title, :anchor => anchor)
163 163 }
164 164 format.api {
165 165 if was_new_page
166 166 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
167 167 else
168 168 render_api_ok
169 169 end
170 170 }
171 171 end
172 172 else
173 173 respond_to do |format|
174 174 format.html { render :action => 'edit' }
175 175 format.api { render_validation_errors(@content) }
176 176 end
177 177 end
178 178
179 179 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
180 180 # Optimistic locking exception
181 181 respond_to do |format|
182 182 format.html {
183 183 flash.now[:error] = l(:notice_locking_conflict)
184 184 render :action => 'edit'
185 185 }
186 186 format.api { render_api_head :conflict }
187 187 end
188 188 end
189 189
190 190 # rename a page
191 191 def rename
192 192 return render_403 unless editable?
193 193 @page.redirect_existing_links = true
194 194 # used to display the *original* title if some AR validation errors occur
195 195 @original_title = @page.pretty_title
196 196 @page.safe_attributes = params[:wiki_page]
197 197 if request.post? && @page.save
198 198 flash[:notice] = l(:notice_successful_update)
199 199 redirect_to project_wiki_page_path(@page.project, @page.title)
200 200 end
201 201 end
202 202
203 203 def protect
204 204 @page.update_attribute :protected, params[:protected]
205 205 redirect_to project_wiki_page_path(@project, @page.title)
206 206 end
207 207
208 208 # show page history
209 209 def history
210 210 @version_count = @page.content.versions.count
211 211 @version_pages = Paginator.new @version_count, per_page_option, params['page']
212 212 # don't load text
213 213 @versions = @page.content.versions.
214 214 select("id, author_id, comments, updated_on, version").
215 215 reorder('version DESC').
216 216 limit(@version_pages.per_page + 1).
217 217 offset(@version_pages.offset).
218 218 to_a
219 219
220 220 render :layout => false if request.xhr?
221 221 end
222 222
223 223 def diff
224 224 @diff = @page.diff(params[:version], params[:version_from])
225 225 render_404 unless @diff
226 226 end
227 227
228 228 def annotate
229 229 @annotate = @page.annotate(params[:version])
230 230 render_404 unless @annotate
231 231 end
232 232
233 233 # Removes a wiki page and its history
234 234 # Children can be either set as root pages, removed or reassigned to another parent page
235 235 def destroy
236 236 return render_403 unless editable?
237 237
238 238 @descendants_count = @page.descendants.size
239 239 if @descendants_count > 0
240 240 case params[:todo]
241 241 when 'nullify'
242 242 # Nothing to do
243 243 when 'destroy'
244 244 # Removes all its descendants
245 245 @page.descendants.each(&:destroy)
246 246 when 'reassign'
247 247 # Reassign children to another parent page
248 248 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
249 249 return unless reassign_to
250 250 @page.children.each do |child|
251 251 child.update_attribute(:parent, reassign_to)
252 252 end
253 253 else
254 254 @reassignable_to = @wiki.pages - @page.self_and_descendants
255 255 # display the destroy form if it's a user request
256 256 return unless api_request?
257 257 end
258 258 end
259 259 @page.destroy
260 260 respond_to do |format|
261 261 format.html { redirect_to project_wiki_index_path(@project) }
262 262 format.api { render_api_ok }
263 263 end
264 264 end
265 265
266 266 def destroy_version
267 267 return render_403 unless editable?
268 268
269 269 if content = @page.content.versions.find_by_version(params[:version])
270 270 content.destroy
271 271 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
272 272 else
273 273 render_404
274 274 end
275 275 end
276 276
277 277 # Export wiki to a single pdf or html file
278 278 def export
279 279 @pages = @wiki.pages.
280 280 order('title').
281 281 includes([:content, {:attachments => :author}]).
282 282 to_a
283 283 respond_to do |format|
284 284 format.html {
285 285 export = render_to_string :action => 'export_multiple', :layout => false
286 286 send_data(export, :type => 'text/html', :filename => "wiki.html")
287 287 }
288 288 format.pdf {
289 289 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}.pdf"
290 290 }
291 291 end
292 292 end
293 293
294 294 def preview
295 295 page = @wiki.find_page(params[:id])
296 296 # page is nil when previewing a new page
297 297 return render_403 unless page.nil? || editable?(page)
298 298 if page
299 299 @attachments += page.attachments
300 300 @previewed = page.content
301 301 end
302 302 @text = params[:content][:text]
303 303 render :partial => 'common/preview'
304 304 end
305 305
306 306 def add_attachment
307 307 return render_403 unless editable?
308 308 attachments = Attachment.attach_files(@page, params[:attachments])
309 309 render_attachment_warning_if_needed(@page)
310 310 redirect_to :action => 'show', :id => @page.title, :project_id => @project
311 311 end
312 312
313 313 private
314 314
315 315 def find_wiki
316 316 @project = Project.find(params[:project_id])
317 317 @wiki = @project.wiki
318 318 render_404 unless @wiki
319 319 rescue ActiveRecord::RecordNotFound
320 320 render_404
321 321 end
322 322
323 323 # Finds the requested page or a new page if it doesn't exist
324 324 def find_existing_or_new_page
325 325 @page = @wiki.find_or_new_page(params[:id])
326 326 if @wiki.page_found_with_redirect?
327 327 redirect_to_page @page
328 328 end
329 329 end
330 330
331 331 # Finds the requested page and returns a 404 error if it doesn't exist
332 332 def find_existing_page
333 333 @page = @wiki.find_page(params[:id])
334 334 if @page.nil?
335 335 render_404
336 336 return
337 337 end
338 338 if @wiki.page_found_with_redirect?
339 339 redirect_to_page @page
340 340 end
341 341 end
342 342
343 343 def redirect_to_page(page)
344 344 if page.project && page.project.visible?
345 345 redirect_to :action => action_name, :project_id => page.project, :id => page.title
346 346 else
347 347 render_404
348 348 end
349 349 end
350 350
351 351 # Returns true if the current user is allowed to edit the page, otherwise false
352 352 def editable?(page = @page)
353 353 page.editable_by?(User.current)
354 354 end
355 355
356 356 # Returns the default content of a new wiki page
357 357 def initial_page_content(page)
358 358 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
359 359 extend helper unless self.instance_of?(helper)
360 360 helper.instance_method(:initial_page_content).bind(self).call(page)
361 361 end
362 362
363 363 def load_pages_for_index
364 364 @pages = @wiki.pages.with_updated_on.
365 365 reorder("#{WikiPage.table_name}.title").
366 366 includes(:wiki => :project).
367 367 includes(:parent).
368 368 to_a
369 369 end
370 370 end
@@ -1,36 +1,36
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 WikisController < ApplicationController
19 19 menu_item :settings
20 20 before_filter :find_project, :authorize
21 21
22 22 # Create or update a project's wiki
23 23 def edit
24 24 @wiki = @project.wiki || Wiki.new(:project => @project)
25 25 @wiki.safe_attributes = params[:wiki]
26 26 @wiki.save if request.post?
27 27 end
28 28
29 29 # Delete a project's wiki
30 30 def destroy
31 31 if request.post? && params[:confirm] && @project.wiki
32 32 @project.wiki.destroy
33 33 redirect_to settings_project_path(@project, :tab => 'wiki')
34 34 end
35 35 end
36 36 end
@@ -1,144 +1,144
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22
23 23 def index
24 24 @roles = Role.sorted.select(&:consider_workflow?)
25 25 @trackers = Tracker.sorted
26 26 @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count
27 27 end
28 28
29 29 def edit
30 30 find_trackers_roles_and_statuses_for_edit
31 31
32 32 if request.post? && @roles && @trackers && params[:transitions]
33 33 transitions = params[:transitions].deep_dup
34 34 transitions.each do |old_status_id, transitions_by_new_status|
35 35 transitions_by_new_status.each do |new_status_id, transition_by_rule|
36 36 transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
37 37 end
38 38 end
39 39 WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
40 40 flash[:notice] = l(:notice_successful_update)
41 41 redirect_to_referer_or workflows_edit_path
42 42 return
43 43 end
44 44
45 45 if @trackers && @roles && @statuses.any?
46 46 workflows = WorkflowTransition.
47 47 where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).
48 48 preload(:old_status, :new_status)
49 49 @workflows = {}
50 50 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
51 51 @workflows['author'] = workflows.select {|w| w.author}
52 52 @workflows['assignee'] = workflows.select {|w| w.assignee}
53 53 end
54 54 end
55 55
56 56 def permissions
57 57 find_trackers_roles_and_statuses_for_edit
58 58
59 59 if request.post? && @roles && @trackers && params[:permissions]
60 60 permissions = params[:permissions].deep_dup
61 61 permissions.each { |field, rule_by_status_id|
62 62 rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
63 63 }
64 64 WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
65 65 flash[:notice] = l(:notice_successful_update)
66 66 redirect_to_referer_or workflows_permissions_path
67 67 return
68 68 end
69 69
70 70 if @roles && @trackers
71 71 @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
72 72 @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
73 73 @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
74 74 @statuses.each {|status| @permissions[status.id] ||= {}}
75 75 end
76 76 end
77 77
78 78 def copy
79 79 @roles = Role.sorted.select(&:consider_workflow?)
80 80 @trackers = Tracker.sorted
81 81
82 82 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
83 83 @source_tracker = nil
84 84 else
85 85 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
86 86 end
87 87 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
88 88 @source_role = nil
89 89 else
90 90 @source_role = Role.find_by_id(params[:source_role_id].to_i)
91 91 end
92 92 @target_trackers = params[:target_tracker_ids].blank? ?
93 93 nil : Tracker.where(:id => params[:target_tracker_ids]).to_a
94 94 @target_roles = params[:target_role_ids].blank? ?
95 95 nil : Role.where(:id => params[:target_role_ids]).to_a
96 96 if request.post?
97 97 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
98 98 flash.now[:error] = l(:error_workflow_copy_source)
99 99 elsif @target_trackers.blank? || @target_roles.blank?
100 100 flash.now[:error] = l(:error_workflow_copy_target)
101 101 else
102 102 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
103 103 flash[:notice] = l(:notice_successful_update)
104 104 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
105 105 end
106 106 end
107 107 end
108 108
109 109 private
110 110
111 111 def find_trackers_roles_and_statuses_for_edit
112 112 find_roles
113 113 find_trackers
114 114 find_statuses
115 115 end
116 116
117 117 def find_roles
118 118 ids = Array.wrap(params[:role_id])
119 119 if ids == ['all']
120 120 @roles = Role.sorted.to_a
121 121 elsif ids.present?
122 122 @roles = Role.where(:id => ids).to_a
123 123 end
124 124 @roles = nil if @roles.blank?
125 125 end
126 126
127 127 def find_trackers
128 128 ids = Array.wrap(params[:tracker_id])
129 129 if ids == ['all']
130 130 @trackers = Tracker.sorted.to_a
131 131 elsif ids.present?
132 132 @trackers = Tracker.where(:id => ids).to_a
133 133 end
134 134 @trackers = nil if @trackers.blank?
135 135 end
136 136
137 137 def find_statuses
138 138 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
139 139 if @trackers && @used_statuses_only
140 140 @statuses = @trackers.map(&:issue_statuses).flatten.uniq.sort.presence
141 141 end
142 142 @statuses ||= IssueStatus.sorted.to_a
143 143 end
144 144 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AccountHelper
21 21 end
@@ -1,33 +1,33
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ActivitiesHelper
21 21 def sort_activity_events(events)
22 22 events_by_group = events.group_by(&:event_group)
23 23 sorted_events = []
24 24 events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event|
25 25 if group_events = events_by_group.delete(event.event_group)
26 26 group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i|
27 27 sorted_events << [e, i > 0]
28 28 end
29 29 end
30 30 end
31 31 sorted_events
32 32 end
33 33 end
@@ -1,35 +1,35
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AdminHelper
21 21 def project_status_options_for_select(selected)
22 22 options_for_select([[l(:label_all), ''],
23 23 [l(:project_status_active), '1'],
24 24 [l(:project_status_closed), '5'],
25 25 [l(:project_status_archived), '9']], selected.to_s)
26 26 end
27 27
28 28 def plugin_data_for_updates(plugins)
29 29 data = {"v" => Redmine::VERSION.to_s, "p" => {}}
30 30 plugins.each do |plugin|
31 31 data["p"].merge! plugin.id => {"v" => plugin.version, "n" => plugin.name, "a" => plugin.author}
32 32 end
33 33 data
34 34 end
35 35 end
@@ -1,1337 +1,1337
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31
32 32 extend Forwardable
33 33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 #
42 42 # @param [String] name Anchor text (passed to link_to)
43 43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 44 # @param [optional, Hash] html_options Options passed to link_to
45 45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active? || (User.current.admin? && user.logged?)
55 55 link_to name, user_path(user), :class => user.css_classes
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 72 #
73 73 def link_to_issue(issue, options={})
74 74 title = nil
75 75 subject = nil
76 76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 77 if options[:subject] == false
78 78 title = issue.subject.truncate(60)
79 79 else
80 80 subject = issue.subject
81 81 if truncate_length = options[:truncate]
82 82 subject = subject.truncate(truncate_length)
83 83 end
84 84 end
85 85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 87 :class => issue.css_classes, :title => title)
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 100 html_options = options.slice!(:only_path)
101 101 options[:only_path] = true unless options.key?(:only_path)
102 102 url = send(route_method, attachment, attachment.filename, options)
103 103 link_to text, url, html_options
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision)),
119 119 :accesskey => options[:accesskey]
120 120 )
121 121 end
122 122
123 123 # Generates a link to a message
124 124 def link_to_message(message, options={}, html_options = nil)
125 125 link_to(
126 126 message.subject.truncate(60),
127 127 board_message_url(message.board_id, message.parent_id || message.id, {
128 128 :r => (message.parent_id && message.id),
129 129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 130 :only_path => true
131 131 }.merge(options)),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 142 #
143 143 def link_to_project(project, options={}, html_options = nil)
144 144 if project.archived?
145 145 h(project.name)
146 146 else
147 147 link_to project.name,
148 148 project_url(project, {:only_path => true}.merge(options)),
149 149 html_options
150 150 end
151 151 end
152 152
153 153 # Generates a link to a project settings if active
154 154 def link_to_project_settings(project, options={}, html_options=nil)
155 155 if project.active?
156 156 link_to project.name, settings_project_path(project, options), html_options
157 157 elsif project.archived?
158 158 h(project.name)
159 159 else
160 160 link_to project.name, project_path(project, options), html_options
161 161 end
162 162 end
163 163
164 164 # Generates a link to a version
165 165 def link_to_version(version, options = {})
166 166 return '' unless version && version.is_a?(Version)
167 167 options = {:title => format_date(version.effective_date)}.merge(options)
168 168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 169 end
170 170
171 171 # Helper that formats object for html or text rendering
172 172 def format_object(object, html=true, &block)
173 173 if block_given?
174 174 object = yield object
175 175 end
176 176 case object.class.name
177 177 when 'Array'
178 178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 179 when 'Time'
180 180 format_time(object)
181 181 when 'Date'
182 182 format_date(object)
183 183 when 'Fixnum'
184 184 object.to_s
185 185 when 'Float'
186 186 sprintf "%.2f", object
187 187 when 'User'
188 188 html ? link_to_user(object) : object.to_s
189 189 when 'Project'
190 190 html ? link_to_project(object) : object.to_s
191 191 when 'Version'
192 192 html ? link_to_version(object) : object.to_s
193 193 when 'TrueClass'
194 194 l(:general_text_Yes)
195 195 when 'FalseClass'
196 196 l(:general_text_No)
197 197 when 'Issue'
198 198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 199 when 'CustomValue', 'CustomFieldValue'
200 200 if object.custom_field
201 201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 202 if f.nil? || f.is_a?(String)
203 203 f
204 204 else
205 205 format_object(f, html, &block)
206 206 end
207 207 else
208 208 object.value.to_s
209 209 end
210 210 else
211 211 html ? h(object) : object.to_s
212 212 end
213 213 end
214 214
215 215 def wiki_page_path(page, options={})
216 216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 217 end
218 218
219 219 def thumbnail_tag(attachment)
220 220 link_to image_tag(thumbnail_path(attachment)),
221 221 named_attachment_path(attachment, attachment.filename),
222 222 :title => attachment.filename
223 223 end
224 224
225 225 def toggle_link(name, id, options={})
226 226 onclick = "$('##{id}').toggle(); "
227 227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 228 onclick << "return false;"
229 229 link_to(name, "#", :onclick => onclick)
230 230 end
231 231
232 232 def format_activity_title(text)
233 233 h(truncate_single_line_raw(text, 100))
234 234 end
235 235
236 236 def format_activity_day(date)
237 237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 238 end
239 239
240 240 def format_activity_description(text)
241 241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 243 end
244 244
245 245 def format_version_name(version)
246 246 if version.project == @project
247 247 h(version)
248 248 else
249 249 h("#{version.project} - #{version}")
250 250 end
251 251 end
252 252
253 253 def due_date_distance_in_words(date)
254 254 if date
255 255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 256 end
257 257 end
258 258
259 259 # Renders a tree of projects as a nested set of unordered lists
260 260 # The given collection may be a subset of the whole project tree
261 261 # (eg. some intermediate nodes are private and can not be seen)
262 262 def render_project_nested_lists(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 original_project = @project
267 267 projects.sort_by(&:lft).each do |project|
268 268 # set the project environment to please macros.
269 269 @project = project
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 classes = (ancestors.empty? ? 'root' : 'child')
281 281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 282 s << h(block_given? ? capture(project, &block) : project.name)
283 283 s << "</div>\n"
284 284 ancestors << project
285 285 end
286 286 s << ("</li></ul>\n" * ancestors.size)
287 287 @project = original_project
288 288 end
289 289 s.html_safe
290 290 end
291 291
292 292 def render_page_hierarchy(pages, node=nil, options={})
293 293 content = ''
294 294 if pages[node]
295 295 content << "<ul class=\"pages-hierarchy\">\n"
296 296 pages[node].each do |page|
297 297 content << "<li>"
298 298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 301 content << "</li>\n"
302 302 end
303 303 content << "</ul>\n"
304 304 end
305 305 content.html_safe
306 306 end
307 307
308 308 # Renders flash messages
309 309 def render_flash_messages
310 310 s = ''
311 311 flash.each do |k,v|
312 312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Renders tabs and their content
318 318 def render_tabs(tabs, selected=params[:tab])
319 319 if tabs.any?
320 320 unless tabs.detect {|tab| tab[:name] == selected}
321 321 selected = nil
322 322 end
323 323 selected ||= tabs.first[:name]
324 324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 325 else
326 326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 327 end
328 328 end
329 329
330 330 # Renders the project quick-jump box
331 331 def render_project_jump_box
332 332 return unless User.current.logged?
333 333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 334 if projects.any?
335 335 options =
336 336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 337 '<option value="" disabled="disabled">---</option>').html_safe
338 338
339 339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 341 end
342 342
343 343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 345 end
346 346 end
347 347
348 348 def project_tree_options_for_select(projects, options = {})
349 349 s = ''.html_safe
350 350 if blank_text = options[:include_blank]
351 351 if blank_text == true
352 352 blank_text = '&nbsp;'.html_safe
353 353 end
354 354 s << content_tag('option', blank_text, :value => '')
355 355 end
356 356 project_tree(projects) do |project, level|
357 357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 358 tag_options = {:value => project.id}
359 359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 360 tag_options[:selected] = 'selected'
361 361 else
362 362 tag_options[:selected] = nil
363 363 end
364 364 tag_options.merge!(yield(project)) if block_given?
365 365 s << content_tag('option', name_prefix + h(project), tag_options)
366 366 end
367 367 s.html_safe
368 368 end
369 369
370 370 # Yields the given block for each project with its level in the tree
371 371 #
372 372 # Wrapper for Project#project_tree
373 373 def project_tree(projects, &block)
374 374 Project.project_tree(projects, &block)
375 375 end
376 376
377 377 def principals_check_box_tags(name, principals)
378 378 s = ''
379 379 principals.each do |principal|
380 380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 381 end
382 382 s.html_safe
383 383 end
384 384
385 385 # Returns a string for users/groups option tags
386 386 def principals_options_for_select(collection, selected=nil)
387 387 s = ''
388 388 if collection.include?(User.current)
389 389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 390 end
391 391 groups = ''
392 392 collection.sort.each do |element|
393 393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 395 end
396 396 unless groups.empty?
397 397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 398 end
399 399 s.html_safe
400 400 end
401 401
402 402 def option_tag(name, text, value, selected=nil, options={})
403 403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 404 end
405 405
406 406 def truncate_single_line_raw(string, length)
407 407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 408 end
409 409
410 410 # Truncates at line break after 250 characters or options[:length]
411 411 def truncate_lines(string, options={})
412 412 length = options[:length] || 250
413 413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 414 "#{$1}..."
415 415 else
416 416 string
417 417 end
418 418 end
419 419
420 420 def anchor(text)
421 421 text.to_s.gsub(' ', '_')
422 422 end
423 423
424 424 def html_hours(text)
425 425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 426 end
427 427
428 428 def authoring(created, author, options={})
429 429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 430 end
431 431
432 432 def time_tag(time)
433 433 text = distance_of_time_in_words(Time.now, time)
434 434 if @project
435 435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 436 else
437 437 content_tag('abbr', text, :title => format_time(time))
438 438 end
439 439 end
440 440
441 441 def syntax_highlight_lines(name, content)
442 442 lines = []
443 443 syntax_highlight(name, content).each_line { |line| lines << line }
444 444 lines
445 445 end
446 446
447 447 def syntax_highlight(name, content)
448 448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 449 end
450 450
451 451 def to_path_param(path)
452 452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 453 str.blank? ? nil : str
454 454 end
455 455
456 456 def reorder_links(name, url, method = :post)
457 457 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
458 458 url.merge({"#{name}[move_to]" => 'highest'}),
459 459 :method => method, :title => l(:label_sort_highest)) +
460 460 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
461 461 url.merge({"#{name}[move_to]" => 'higher'}),
462 462 :method => method, :title => l(:label_sort_higher)) +
463 463 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
464 464 url.merge({"#{name}[move_to]" => 'lower'}),
465 465 :method => method, :title => l(:label_sort_lower)) +
466 466 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
467 467 url.merge({"#{name}[move_to]" => 'lowest'}),
468 468 :method => method, :title => l(:label_sort_lowest))
469 469 end
470 470
471 471 def breadcrumb(*args)
472 472 elements = args.flatten
473 473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474 474 end
475 475
476 476 def other_formats_links(&block)
477 477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478 478 yield Redmine::Views::OtherFormatsBuilder.new(self)
479 479 concat('</p>'.html_safe)
480 480 end
481 481
482 482 def page_header_title
483 483 if @project.nil? || @project.new_record?
484 484 h(Setting.app_title)
485 485 else
486 486 b = []
487 487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
488 488 if ancestors.any?
489 489 root = ancestors.shift
490 490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
491 491 if ancestors.size > 2
492 492 b << "\xe2\x80\xa6"
493 493 ancestors = ancestors[-2, 2]
494 494 end
495 495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
496 496 end
497 497 b << h(@project)
498 498 b.join(" \xc2\xbb ").html_safe
499 499 end
500 500 end
501 501
502 502 # Returns a h2 tag and sets the html title with the given arguments
503 503 def title(*args)
504 504 strings = args.map do |arg|
505 505 if arg.is_a?(Array) && arg.size >= 2
506 506 link_to(*arg)
507 507 else
508 508 h(arg.to_s)
509 509 end
510 510 end
511 511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
512 512 content_tag('h2', strings.join(' &#187; ').html_safe)
513 513 end
514 514
515 515 # Sets the html title
516 516 # Returns the html title when called without arguments
517 517 # Current project name and app_title and automatically appended
518 518 # Exemples:
519 519 # html_title 'Foo', 'Bar'
520 520 # html_title # => 'Foo - Bar - My Project - Redmine'
521 521 def html_title(*args)
522 522 if args.empty?
523 523 title = @html_title || []
524 524 title << @project.name if @project
525 525 title << Setting.app_title unless Setting.app_title == title.last
526 526 title.reject(&:blank?).join(' - ')
527 527 else
528 528 @html_title ||= []
529 529 @html_title += args
530 530 end
531 531 end
532 532
533 533 # Returns the theme, controller name, and action as css classes for the
534 534 # HTML body.
535 535 def body_css_classes
536 536 css = []
537 537 if theme = Redmine::Themes.theme(Setting.ui_theme)
538 538 css << 'theme-' + theme.name
539 539 end
540 540
541 541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
542 542 css << 'controller-' + controller_name
543 543 css << 'action-' + action_name
544 544 css.join(' ')
545 545 end
546 546
547 547 def accesskey(s)
548 548 @used_accesskeys ||= []
549 549 key = Redmine::AccessKeys.key_for(s)
550 550 return nil if @used_accesskeys.include?(key)
551 551 @used_accesskeys << key
552 552 key
553 553 end
554 554
555 555 # Formats text according to system settings.
556 556 # 2 ways to call this method:
557 557 # * with a String: textilizable(text, options)
558 558 # * with an object and one of its attribute: textilizable(issue, :description, options)
559 559 def textilizable(*args)
560 560 options = args.last.is_a?(Hash) ? args.pop : {}
561 561 case args.size
562 562 when 1
563 563 obj = options[:object]
564 564 text = args.shift
565 565 when 2
566 566 obj = args.shift
567 567 attr = args.shift
568 568 text = obj.send(attr).to_s
569 569 else
570 570 raise ArgumentError, 'invalid arguments to textilizable'
571 571 end
572 572 return '' if text.blank?
573 573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
574 574 @only_path = only_path = options.delete(:only_path) == false ? false : true
575 575
576 576 text = text.dup
577 577 macros = catch_macros(text)
578 578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
579 579
580 580 @parsed_headings = []
581 581 @heading_anchors = {}
582 582 @current_section = 0 if options[:edit_section_links]
583 583
584 584 parse_sections(text, project, obj, attr, only_path, options)
585 585 text = parse_non_pre_blocks(text, obj, macros) do |text|
586 586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
587 587 send method_name, text, project, obj, attr, only_path, options
588 588 end
589 589 end
590 590 parse_headings(text, project, obj, attr, only_path, options)
591 591
592 592 if @parsed_headings.any?
593 593 replace_toc(text, @parsed_headings)
594 594 end
595 595
596 596 text.html_safe
597 597 end
598 598
599 599 def parse_non_pre_blocks(text, obj, macros)
600 600 s = StringScanner.new(text)
601 601 tags = []
602 602 parsed = ''
603 603 while !s.eos?
604 604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
605 605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
606 606 if tags.empty?
607 607 yield text
608 608 inject_macros(text, obj, macros) if macros.any?
609 609 else
610 610 inject_macros(text, obj, macros, false) if macros.any?
611 611 end
612 612 parsed << text
613 613 if tag
614 614 if closing
615 615 if tags.last && tags.last.casecmp(tag) == 0
616 616 tags.pop
617 617 end
618 618 else
619 619 tags << tag.downcase
620 620 end
621 621 parsed << full_tag
622 622 end
623 623 end
624 624 # Close any non closing tags
625 625 while tag = tags.pop
626 626 parsed << "</#{tag}>"
627 627 end
628 628 parsed
629 629 end
630 630
631 631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
632 632 return if options[:inline_attachments] == false
633 633
634 634 # when using an image link, try to use an attachment, if possible
635 635 attachments = options[:attachments] || []
636 636 attachments += obj.attachments if obj.respond_to?(:attachments)
637 637 if attachments.present?
638 638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
639 639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
640 640 # search for the picture in attachments
641 641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
642 642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
643 643 desc = found.description.to_s.gsub('"', '')
644 644 if !desc.blank? && alttext.blank?
645 645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
646 646 end
647 647 "src=\"#{image_url}\"#{alt}"
648 648 else
649 649 m
650 650 end
651 651 end
652 652 end
653 653 end
654 654
655 655 # Wiki links
656 656 #
657 657 # Examples:
658 658 # [[mypage]]
659 659 # [[mypage|mytext]]
660 660 # wiki links can refer other project wikis, using project name or identifier:
661 661 # [[project:]] -> wiki starting page
662 662 # [[project:|mytext]]
663 663 # [[project:mypage]]
664 664 # [[project:mypage|mytext]]
665 665 def parse_wiki_links(text, project, obj, attr, only_path, options)
666 666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
667 667 link_project = project
668 668 esc, all, page, title = $1, $2, $3, $5
669 669 if esc.nil?
670 670 if page =~ /^([^\:]+)\:(.*)$/
671 671 identifier, page = $1, $2
672 672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
673 673 title ||= identifier if page.blank?
674 674 end
675 675
676 676 if link_project && link_project.wiki
677 677 # extract anchor
678 678 anchor = nil
679 679 if page =~ /^(.+?)\#(.+)$/
680 680 page, anchor = $1, $2
681 681 end
682 682 anchor = sanitize_anchor_name(anchor) if anchor.present?
683 683 # check if page exists
684 684 wiki_page = link_project.wiki.find_page(page)
685 685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
686 686 "##{anchor}"
687 687 else
688 688 case options[:wiki_links]
689 689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
690 690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
691 691 else
692 692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
693 693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
694 694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
695 695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
696 696 end
697 697 end
698 698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
699 699 else
700 700 # project or wiki doesn't exist
701 701 all
702 702 end
703 703 else
704 704 all
705 705 end
706 706 end
707 707 end
708 708
709 709 # Redmine links
710 710 #
711 711 # Examples:
712 712 # Issues:
713 713 # #52 -> Link to issue #52
714 714 # Changesets:
715 715 # r52 -> Link to revision 52
716 716 # commit:a85130f -> Link to scmid starting with a85130f
717 717 # Documents:
718 718 # document#17 -> Link to document with id 17
719 719 # document:Greetings -> Link to the document with title "Greetings"
720 720 # document:"Some document" -> Link to the document with title "Some document"
721 721 # Versions:
722 722 # version#3 -> Link to version with id 3
723 723 # version:1.0.0 -> Link to version named "1.0.0"
724 724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
725 725 # Attachments:
726 726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
727 727 # Source files:
728 728 # source:some/file -> Link to the file located at /some/file in the project's repository
729 729 # source:some/file@52 -> Link to the file's revision 52
730 730 # source:some/file#L120 -> Link to line 120 of the file
731 731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
732 732 # export:some/file -> Force the download of the file
733 733 # Forum messages:
734 734 # message#1218 -> Link to message with id 1218
735 735 # Projects:
736 736 # project:someproject -> Link to project named "someproject"
737 737 # project#3 -> Link to project with id 3
738 738 #
739 739 # Links can refer other objects from other projects, using project identifier:
740 740 # identifier:r52
741 741 # identifier:document:"Some document"
742 742 # identifier:version:1.0.0
743 743 # identifier:source:some/file
744 744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
745 745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
746 746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
747 747 if tag_content
748 748 $&
749 749 else
750 750 link = nil
751 751 project = default_project
752 752 if project_identifier
753 753 project = Project.visible.find_by_identifier(project_identifier)
754 754 end
755 755 if esc.nil?
756 756 if prefix.nil? && sep == 'r'
757 757 if project
758 758 repository = nil
759 759 if repo_identifier
760 760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 761 else
762 762 repository = project.repository
763 763 end
764 764 # project.changesets.visible raises an SQL error because of a double join on repositories
765 765 if repository &&
766 766 (changeset = Changeset.visible.
767 767 find_by_repository_id_and_revision(repository.id, identifier))
768 768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 769 {:only_path => only_path, :controller => 'repositories',
770 770 :action => 'revision', :id => project,
771 771 :repository_id => repository.identifier_param,
772 772 :rev => changeset.revision},
773 773 :class => 'changeset',
774 774 :title => truncate_single_line_raw(changeset.comments, 100))
775 775 end
776 776 end
777 777 elsif sep == '#'
778 778 oid = identifier.to_i
779 779 case prefix
780 780 when nil
781 781 if oid.to_s == identifier &&
782 782 issue = Issue.visible.find_by_id(oid)
783 783 anchor = comment_id ? "note-#{comment_id}" : nil
784 784 link = link_to("##{oid}#{comment_suffix}",
785 785 issue_url(issue, :only_path => only_path, :anchor => anchor),
786 786 :class => issue.css_classes,
787 787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
788 788 end
789 789 when 'document'
790 790 if document = Document.visible.find_by_id(oid)
791 791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
792 792 end
793 793 when 'version'
794 794 if version = Version.visible.find_by_id(oid)
795 795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
796 796 end
797 797 when 'message'
798 798 if message = Message.visible.find_by_id(oid)
799 799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
800 800 end
801 801 when 'forum'
802 802 if board = Board.visible.find_by_id(oid)
803 803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
804 804 end
805 805 when 'news'
806 806 if news = News.visible.find_by_id(oid)
807 807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
808 808 end
809 809 when 'project'
810 810 if p = Project.visible.find_by_id(oid)
811 811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
812 812 end
813 813 end
814 814 elsif sep == ':'
815 815 # removes the double quotes if any
816 816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
817 817 name = CGI.unescapeHTML(name)
818 818 case prefix
819 819 when 'document'
820 820 if project && document = project.documents.visible.find_by_title(name)
821 821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
822 822 end
823 823 when 'version'
824 824 if project && version = project.versions.visible.find_by_name(name)
825 825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
826 826 end
827 827 when 'forum'
828 828 if project && board = project.boards.visible.find_by_name(name)
829 829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 830 end
831 831 when 'news'
832 832 if project && news = project.news.visible.find_by_title(name)
833 833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 834 end
835 835 when 'commit', 'source', 'export'
836 836 if project
837 837 repository = nil
838 838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
839 839 repo_prefix, repo_identifier, name = $1, $2, $3
840 840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
841 841 else
842 842 repository = project.repository
843 843 end
844 844 if prefix == 'commit'
845 845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
846 846 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
847 847 :class => 'changeset',
848 848 :title => truncate_single_line_raw(changeset.comments, 100)
849 849 end
850 850 else
851 851 if repository && User.current.allowed_to?(:browse_repository, project)
852 852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
853 853 path, rev, anchor = $1, $3, $5
854 854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
855 855 :path => to_path_param(path),
856 856 :rev => rev,
857 857 :anchor => anchor},
858 858 :class => (prefix == 'export' ? 'source download' : 'source')
859 859 end
860 860 end
861 861 repo_prefix = nil
862 862 end
863 863 when 'attachment'
864 864 attachments = options[:attachments] || []
865 865 attachments += obj.attachments if obj.respond_to?(:attachments)
866 866 if attachments && attachment = Attachment.latest_attach(attachments, name)
867 867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
868 868 end
869 869 when 'project'
870 870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
871 871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
872 872 end
873 873 end
874 874 end
875 875 end
876 876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
877 877 end
878 878 end
879 879 end
880 880
881 881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
882 882
883 883 def parse_sections(text, project, obj, attr, only_path, options)
884 884 return unless options[:edit_section_links]
885 885 text.gsub!(HEADING_RE) do
886 886 heading, level = $1, $2
887 887 @current_section += 1
888 888 if @current_section > 1
889 889 content_tag('div',
890 890 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
891 891 :class => "contextual heading-#{level}",
892 892 :title => l(:button_edit_section),
893 893 :id => "section-#{@current_section}") + heading.html_safe
894 894 else
895 895 heading
896 896 end
897 897 end
898 898 end
899 899
900 900 # Headings and TOC
901 901 # Adds ids and links to headings unless options[:headings] is set to false
902 902 def parse_headings(text, project, obj, attr, only_path, options)
903 903 return if options[:headings] == false
904 904
905 905 text.gsub!(HEADING_RE) do
906 906 level, attrs, content = $2.to_i, $3, $4
907 907 item = strip_tags(content).strip
908 908 anchor = sanitize_anchor_name(item)
909 909 # used for single-file wiki export
910 910 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
911 911 @heading_anchors[anchor] ||= 0
912 912 idx = (@heading_anchors[anchor] += 1)
913 913 if idx > 1
914 914 anchor = "#{anchor}-#{idx}"
915 915 end
916 916 @parsed_headings << [level, anchor, item]
917 917 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
918 918 end
919 919 end
920 920
921 921 MACROS_RE = /(
922 922 (!)? # escaping
923 923 (
924 924 \{\{ # opening tag
925 925 ([\w]+) # macro name
926 926 (\(([^\n\r]*?)\))? # optional arguments
927 927 ([\n\r].*?[\n\r])? # optional block of text
928 928 \}\} # closing tag
929 929 )
930 930 )/mx unless const_defined?(:MACROS_RE)
931 931
932 932 MACRO_SUB_RE = /(
933 933 \{\{
934 934 macro\((\d+)\)
935 935 \}\}
936 936 )/x unless const_defined?(:MACRO_SUB_RE)
937 937
938 938 # Extracts macros from text
939 939 def catch_macros(text)
940 940 macros = {}
941 941 text.gsub!(MACROS_RE) do
942 942 all, macro = $1, $4.downcase
943 943 if macro_exists?(macro) || all =~ MACRO_SUB_RE
944 944 index = macros.size
945 945 macros[index] = all
946 946 "{{macro(#{index})}}"
947 947 else
948 948 all
949 949 end
950 950 end
951 951 macros
952 952 end
953 953
954 954 # Executes and replaces macros in text
955 955 def inject_macros(text, obj, macros, execute=true)
956 956 text.gsub!(MACRO_SUB_RE) do
957 957 all, index = $1, $2.to_i
958 958 orig = macros.delete(index)
959 959 if execute && orig && orig =~ MACROS_RE
960 960 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
961 961 if esc.nil?
962 962 h(exec_macro(macro, obj, args, block) || all)
963 963 else
964 964 h(all)
965 965 end
966 966 elsif orig
967 967 h(orig)
968 968 else
969 969 h(all)
970 970 end
971 971 end
972 972 end
973 973
974 974 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
975 975
976 976 # Renders the TOC with given headings
977 977 def replace_toc(text, headings)
978 978 text.gsub!(TOC_RE) do
979 979 left_align, right_align = $2, $3
980 980 # Keep only the 4 first levels
981 981 headings = headings.select{|level, anchor, item| level <= 4}
982 982 if headings.empty?
983 983 ''
984 984 else
985 985 div_class = 'toc'
986 986 div_class << ' right' if right_align
987 987 div_class << ' left' if left_align
988 988 out = "<ul class=\"#{div_class}\"><li>"
989 989 root = headings.map(&:first).min
990 990 current = root
991 991 started = false
992 992 headings.each do |level, anchor, item|
993 993 if level > current
994 994 out << '<ul><li>' * (level - current)
995 995 elsif level < current
996 996 out << "</li></ul>\n" * (current - level) + "</li><li>"
997 997 elsif started
998 998 out << '</li><li>'
999 999 end
1000 1000 out << "<a href=\"##{anchor}\">#{item}</a>"
1001 1001 current = level
1002 1002 started = true
1003 1003 end
1004 1004 out << '</li></ul>' * (current - root)
1005 1005 out << '</li></ul>'
1006 1006 end
1007 1007 end
1008 1008 end
1009 1009
1010 1010 # Same as Rails' simple_format helper without using paragraphs
1011 1011 def simple_format_without_paragraph(text)
1012 1012 text.to_s.
1013 1013 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1014 1014 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1015 1015 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1016 1016 html_safe
1017 1017 end
1018 1018
1019 1019 def lang_options_for_select(blank=true)
1020 1020 (blank ? [["(auto)", ""]] : []) + languages_options
1021 1021 end
1022 1022
1023 1023 def labelled_form_for(*args, &proc)
1024 1024 args << {} unless args.last.is_a?(Hash)
1025 1025 options = args.last
1026 1026 if args.first.is_a?(Symbol)
1027 1027 options.merge!(:as => args.shift)
1028 1028 end
1029 1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 1030 form_for(*args, &proc)
1031 1031 end
1032 1032
1033 1033 def labelled_fields_for(*args, &proc)
1034 1034 args << {} unless args.last.is_a?(Hash)
1035 1035 options = args.last
1036 1036 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1037 1037 fields_for(*args, &proc)
1038 1038 end
1039 1039
1040 1040 def error_messages_for(*objects)
1041 1041 html = ""
1042 1042 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1043 1043 errors = objects.map {|o| o.errors.full_messages}.flatten
1044 1044 if errors.any?
1045 1045 html << "<div id='errorExplanation'><ul>\n"
1046 1046 errors.each do |error|
1047 1047 html << "<li>#{h error}</li>\n"
1048 1048 end
1049 1049 html << "</ul></div>\n"
1050 1050 end
1051 1051 html.html_safe
1052 1052 end
1053 1053
1054 1054 def delete_link(url, options={})
1055 1055 options = {
1056 1056 :method => :delete,
1057 1057 :data => {:confirm => l(:text_are_you_sure)},
1058 1058 :class => 'icon icon-del'
1059 1059 }.merge(options)
1060 1060
1061 1061 link_to l(:button_delete), url, options
1062 1062 end
1063 1063
1064 1064 def preview_link(url, form, target='preview', options={})
1065 1065 content_tag 'a', l(:label_preview), {
1066 1066 :href => "#",
1067 1067 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1068 1068 :accesskey => accesskey(:preview)
1069 1069 }.merge(options)
1070 1070 end
1071 1071
1072 1072 def link_to_function(name, function, html_options={})
1073 1073 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1074 1074 end
1075 1075
1076 1076 # Helper to render JSON in views
1077 1077 def raw_json(arg)
1078 1078 arg.to_json.to_s.gsub('/', '\/').html_safe
1079 1079 end
1080 1080
1081 1081 def back_url
1082 1082 url = params[:back_url]
1083 1083 if url.nil? && referer = request.env['HTTP_REFERER']
1084 1084 url = CGI.unescape(referer.to_s)
1085 1085 end
1086 1086 url
1087 1087 end
1088 1088
1089 1089 def back_url_hidden_field_tag
1090 1090 url = back_url
1091 1091 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1092 1092 end
1093 1093
1094 1094 def check_all_links(form_name)
1095 1095 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1096 1096 " | ".html_safe +
1097 1097 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1098 1098 end
1099 1099
1100 1100 def toggle_checkboxes_link(selector)
1101 1101 link_to_function image_tag('toggle_check.png'),
1102 1102 "toggleCheckboxesBySelector('#{selector}')",
1103 1103 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1104 1104 end
1105 1105
1106 1106 def progress_bar(pcts, options={})
1107 1107 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1108 1108 pcts = pcts.collect(&:round)
1109 1109 pcts[1] = pcts[1] - pcts[0]
1110 1110 pcts << (100 - pcts[1] - pcts[0])
1111 1111 legend = options[:legend] || ''
1112 1112 content_tag('table',
1113 1113 content_tag('tr',
1114 1114 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1115 1115 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1116 1116 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1117 1117 ), :class => "progress progress-#{pcts[0]}").html_safe +
1118 1118 content_tag('p', legend, :class => 'percent').html_safe
1119 1119 end
1120 1120
1121 1121 def checked_image(checked=true)
1122 1122 if checked
1123 1123 @checked_image_tag ||= image_tag('toggle_check.png')
1124 1124 end
1125 1125 end
1126 1126
1127 1127 def context_menu(url)
1128 1128 unless @context_menu_included
1129 1129 content_for :header_tags do
1130 1130 javascript_include_tag('context_menu') +
1131 1131 stylesheet_link_tag('context_menu')
1132 1132 end
1133 1133 if l(:direction) == 'rtl'
1134 1134 content_for :header_tags do
1135 1135 stylesheet_link_tag('context_menu_rtl')
1136 1136 end
1137 1137 end
1138 1138 @context_menu_included = true
1139 1139 end
1140 1140 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1141 1141 end
1142 1142
1143 1143 def calendar_for(field_id)
1144 1144 include_calendar_headers_tags
1145 1145 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1146 1146 end
1147 1147
1148 1148 def include_calendar_headers_tags
1149 1149 unless @calendar_headers_tags_included
1150 1150 tags = ''.html_safe
1151 1151 @calendar_headers_tags_included = true
1152 1152 content_for :header_tags do
1153 1153 start_of_week = Setting.start_of_week
1154 1154 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1155 1155 # Redmine uses 1..7 (monday..sunday) in settings and locales
1156 1156 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1157 1157 start_of_week = start_of_week.to_i % 7
1158 1158 tags << javascript_tag(
1159 1159 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1160 1160 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1161 1161 path_to_image('/images/calendar.png') +
1162 1162 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1163 1163 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1164 1164 "beforeShow: beforeShowDatePicker};")
1165 1165 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1166 1166 unless jquery_locale == 'en'
1167 1167 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1168 1168 end
1169 1169 tags
1170 1170 end
1171 1171 end
1172 1172 end
1173 1173
1174 1174 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1175 1175 # Examples:
1176 1176 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1177 1177 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1178 1178 #
1179 1179 def stylesheet_link_tag(*sources)
1180 1180 options = sources.last.is_a?(Hash) ? sources.pop : {}
1181 1181 plugin = options.delete(:plugin)
1182 1182 sources = sources.map do |source|
1183 1183 if plugin
1184 1184 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1185 1185 elsif current_theme && current_theme.stylesheets.include?(source)
1186 1186 current_theme.stylesheet_path(source)
1187 1187 else
1188 1188 source
1189 1189 end
1190 1190 end
1191 1191 super *sources, options
1192 1192 end
1193 1193
1194 1194 # Overrides Rails' image_tag with themes and plugins support.
1195 1195 # Examples:
1196 1196 # image_tag('image.png') # => picks image.png from the current theme or defaults
1197 1197 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1198 1198 #
1199 1199 def image_tag(source, options={})
1200 1200 if plugin = options.delete(:plugin)
1201 1201 source = "/plugin_assets/#{plugin}/images/#{source}"
1202 1202 elsif current_theme && current_theme.images.include?(source)
1203 1203 source = current_theme.image_path(source)
1204 1204 end
1205 1205 super source, options
1206 1206 end
1207 1207
1208 1208 # Overrides Rails' javascript_include_tag with plugins support
1209 1209 # Examples:
1210 1210 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1211 1211 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1212 1212 #
1213 1213 def javascript_include_tag(*sources)
1214 1214 options = sources.last.is_a?(Hash) ? sources.pop : {}
1215 1215 if plugin = options.delete(:plugin)
1216 1216 sources = sources.map do |source|
1217 1217 if plugin
1218 1218 "/plugin_assets/#{plugin}/javascripts/#{source}"
1219 1219 else
1220 1220 source
1221 1221 end
1222 1222 end
1223 1223 end
1224 1224 super *sources, options
1225 1225 end
1226 1226
1227 1227 def sidebar_content?
1228 1228 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1229 1229 end
1230 1230
1231 1231 def view_layouts_base_sidebar_hook_response
1232 1232 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1233 1233 end
1234 1234
1235 1235 def email_delivery_enabled?
1236 1236 !!ActionMailer::Base.perform_deliveries
1237 1237 end
1238 1238
1239 1239 # Returns the avatar image tag for the given +user+ if avatars are enabled
1240 1240 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1241 1241 def avatar(user, options = { })
1242 1242 if Setting.gravatar_enabled?
1243 1243 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1244 1244 email = nil
1245 1245 if user.respond_to?(:mail)
1246 1246 email = user.mail
1247 1247 elsif user.to_s =~ %r{<(.+?)>}
1248 1248 email = $1
1249 1249 end
1250 1250 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1251 1251 else
1252 1252 ''
1253 1253 end
1254 1254 end
1255 1255
1256 1256 # Returns a link to edit user's avatar if avatars are enabled
1257 1257 def avatar_edit_link(user, options={})
1258 1258 if Setting.gravatar_enabled?
1259 1259 url = "https://gravatar.com"
1260 1260 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1261 1261 end
1262 1262 end
1263 1263
1264 1264 def sanitize_anchor_name(anchor)
1265 1265 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1266 1266 end
1267 1267
1268 1268 # Returns the javascript tags that are included in the html layout head
1269 1269 def javascript_heads
1270 1270 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1271 1271 unless User.current.pref.warn_on_leaving_unsaved == '0'
1272 1272 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1273 1273 end
1274 1274 tags
1275 1275 end
1276 1276
1277 1277 def favicon
1278 1278 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1279 1279 end
1280 1280
1281 1281 # Returns the path to the favicon
1282 1282 def favicon_path
1283 1283 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1284 1284 image_path(icon)
1285 1285 end
1286 1286
1287 1287 # Returns the full URL to the favicon
1288 1288 def favicon_url
1289 1289 # TODO: use #image_url introduced in Rails4
1290 1290 path = favicon_path
1291 1291 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1292 1292 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1293 1293 end
1294 1294
1295 1295 def robot_exclusion_tag
1296 1296 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1297 1297 end
1298 1298
1299 1299 # Returns true if arg is expected in the API response
1300 1300 def include_in_api_response?(arg)
1301 1301 unless @included_in_api_response
1302 1302 param = params[:include]
1303 1303 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1304 1304 @included_in_api_response.collect!(&:strip)
1305 1305 end
1306 1306 @included_in_api_response.include?(arg.to_s)
1307 1307 end
1308 1308
1309 1309 # Returns options or nil if nometa param or X-Redmine-Nometa header
1310 1310 # was set in the request
1311 1311 def api_meta(options)
1312 1312 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1313 1313 # compatibility mode for activeresource clients that raise
1314 1314 # an error when deserializing an array with attributes
1315 1315 nil
1316 1316 else
1317 1317 options
1318 1318 end
1319 1319 end
1320 1320
1321 1321 def generate_csv(&block)
1322 1322 decimal_separator = l(:general_csv_decimal_separator)
1323 1323 encoding = l(:general_csv_encoding)
1324 1324 end
1325 1325
1326 1326 private
1327 1327
1328 1328 def wiki_helper
1329 1329 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1330 1330 extend helper
1331 1331 return self
1332 1332 end
1333 1333
1334 1334 def link_to_content_update(text, url_params = {}, html_options = {})
1335 1335 link_to(text, url_params, html_options)
1336 1336 end
1337 1337 end
@@ -1,69 +1,69
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AttachmentsHelper
21 21
22 22 def container_attachments_edit_path(container)
23 23 object_attachments_edit_path container.class.name.underscore.pluralize, container.id
24 24 end
25 25
26 26 def container_attachments_path(container)
27 27 object_attachments_path container.class.name.underscore.pluralize, container.id
28 28 end
29 29
30 30 # Displays view/delete links to the attachments of the given object
31 31 # Options:
32 32 # :author -- author names are not displayed if set to false
33 33 # :thumbails -- display thumbnails if enabled in settings
34 34 def link_to_attachments(container, options = {})
35 35 options.assert_valid_keys(:author, :thumbnails)
36 36
37 37 attachments = container.attachments.preload(:author).to_a
38 38 if attachments.any?
39 39 options = {
40 40 :editable => container.attachments_editable?,
41 41 :deletable => container.attachments_deletable?,
42 42 :author => true
43 43 }.merge(options)
44 44 render :partial => 'attachments/links',
45 45 :locals => {
46 46 :container => container,
47 47 :attachments => attachments,
48 48 :options => options,
49 49 :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)
50 50 }
51 51 end
52 52 end
53 53
54 54 def render_api_attachment(attachment, api)
55 55 api.attachment do
56 56 api.id attachment.id
57 57 api.filename attachment.filename
58 58 api.filesize attachment.filesize
59 59 api.content_type attachment.content_type
60 60 api.description attachment.description
61 61 api.content_url download_named_attachment_url(attachment, attachment.filename)
62 62 if attachment.thumbnailable?
63 63 api.thumbnail_url thumbnail_url(attachment)
64 64 end
65 65 api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
66 66 api.created_on attachment.created_on
67 67 end
68 68 end
69 69 end
@@ -1,24 +1,24
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AuthSourcesHelper
21 21 def auth_source_partial_name(auth_source)
22 22 "form_#{auth_source.class.name.underscore}"
23 23 end
24 24 end
@@ -1,41 +1,41
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module BoardsHelper
21 21 def board_breadcrumb(item)
22 22 board = item.is_a?(Message) ? item.board : item
23 23 links = [link_to(l(:label_board_plural), project_boards_path(item.project))]
24 24 boards = board.ancestors.reverse
25 25 if item.is_a?(Message)
26 26 boards << board
27 27 end
28 28 links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))}
29 29 breadcrumb links
30 30 end
31 31
32 32 def boards_options_for_select(boards)
33 33 options = []
34 34 Board.board_tree(boards) do |board, level|
35 35 label = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
36 36 label << board.name
37 37 options << [label, board.id]
38 38 end
39 39 options
40 40 end
41 41 end
@@ -1,58 +1,58
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module CalendarsHelper
21 21 def link_to_previous_month(year, month, options={})
22 22 target_year, target_month = if month == 1
23 23 [year - 1, 12]
24 24 else
25 25 [year, month - 1]
26 26 end
27 27
28 28 name = if target_month == 12
29 29 "#{month_name(target_month)} #{target_year}"
30 30 else
31 31 "#{month_name(target_month)}"
32 32 end
33 33
34 34 # \xc2\xab(utf-8) = &#171;
35 35 link_to_month(("\xc2\xab " + name), target_year, target_month, options)
36 36 end
37 37
38 38 def link_to_next_month(year, month, options={})
39 39 target_year, target_month = if month == 12
40 40 [year + 1, 1]
41 41 else
42 42 [year, month + 1]
43 43 end
44 44
45 45 name = if target_month == 1
46 46 "#{month_name(target_month)} #{target_year}"
47 47 else
48 48 "#{month_name(target_month)}"
49 49 end
50 50
51 51 # \xc2\xbb(utf-8) = &#187;
52 52 link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
53 53 end
54 54
55 55 def link_to_month(link_name, year, month, options={})
56 56 link_to_content_update(h(link_name), params.merge(:year => year, :month => month), options)
57 57 end
58 58 end
@@ -1,50 +1,50
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ContextMenusHelper
21 21 def context_menu_link(name, url, options={})
22 22 options[:class] ||= ''
23 23 if options.delete(:selected)
24 24 options[:class] << ' icon-checked disabled'
25 25 options[:disabled] = true
26 26 end
27 27 if options.delete(:disabled)
28 28 options.delete(:method)
29 29 options.delete(:data)
30 30 options[:onclick] = 'return false;'
31 31 options[:class] << ' disabled'
32 32 url = '#'
33 33 end
34 34 link_to h(name), url, options
35 35 end
36 36
37 37 def bulk_update_custom_field_context_menu_link(field, text, value)
38 38 context_menu_link h(text),
39 39 bulk_update_issues_path(:ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back),
40 40 :method => :post,
41 41 :selected => (@issue && @issue.custom_field_value(field) == value)
42 42 end
43 43
44 44 def bulk_update_time_entry_custom_field_context_menu_link(field, text, value)
45 45 context_menu_link h(text),
46 46 bulk_update_time_entries_path(:ids => @time_entries.map(&:id).sort, :time_entry => {'custom_field_values' => {field.id => value}}, :back_url => @back),
47 47 :method => :post,
48 48 :selected => (@time_entry && @time_entry.custom_field_value(field) == value)
49 49 end
50 50 end
@@ -1,176 +1,176
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module CustomFieldsHelper
21 21
22 22 CUSTOM_FIELDS_TABS = [
23 23 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
24 24 :label => :label_issue_plural},
25 25 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
26 26 :label => :label_spent_time},
27 27 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
28 28 :label => :label_project_plural},
29 29 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
30 30 :label => :label_version_plural},
31 31 {:name => 'DocumentCustomField', :partial => 'custom_fields/index',
32 32 :label => :label_document_plural},
33 33 {:name => 'UserCustomField', :partial => 'custom_fields/index',
34 34 :label => :label_user_plural},
35 35 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
36 36 :label => :label_group_plural},
37 37 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
38 38 :label => TimeEntryActivity::OptionName},
39 39 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
40 40 :label => IssuePriority::OptionName},
41 41 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
42 42 :label => DocumentCategory::OptionName}
43 43 ]
44 44
45 45 def render_custom_fields_tabs(types)
46 46 tabs = CUSTOM_FIELDS_TABS.select {|h| types.include?(h[:name]) }
47 47 render_tabs tabs
48 48 end
49 49
50 50 def custom_field_type_options
51 51 CUSTOM_FIELDS_TABS.map {|h| [l(h[:label]), h[:name]]}
52 52 end
53 53
54 54 def custom_field_title(custom_field)
55 55 items = []
56 56 items << [l(:label_custom_field_plural), custom_fields_path]
57 57 items << [l(custom_field.type_name), custom_fields_path(:tab => custom_field.class.name)] if custom_field
58 58 items << (custom_field.nil? || custom_field.new_record? ? l(:label_custom_field_new) : custom_field.name)
59 59
60 60 title(*items)
61 61 end
62 62
63 63 def render_custom_field_format_partial(form, custom_field)
64 64 partial = custom_field.format.form_partial
65 65 if partial
66 66 render :partial => custom_field.format.form_partial, :locals => {:f => form, :custom_field => custom_field}
67 67 end
68 68 end
69 69
70 70 def custom_field_tag_name(prefix, custom_field)
71 71 name = "#{prefix}[custom_field_values][#{custom_field.id}]"
72 72 name << "[]" if custom_field.multiple?
73 73 name
74 74 end
75 75
76 76 def custom_field_tag_id(prefix, custom_field)
77 77 "#{prefix}_custom_field_values_#{custom_field.id}"
78 78 end
79 79
80 80 # Return custom field html tag corresponding to its format
81 81 def custom_field_tag(prefix, custom_value)
82 82 custom_value.custom_field.format.edit_tag self,
83 83 custom_field_tag_id(prefix, custom_value.custom_field),
84 84 custom_field_tag_name(prefix, custom_value.custom_field),
85 85 custom_value,
86 86 :class => "#{custom_value.custom_field.field_format}_cf"
87 87 end
88 88
89 89 # Return custom field name tag
90 90 def custom_field_name_tag(custom_field)
91 91 title = custom_field.description.presence
92 92 css = title ? "field-description" : nil
93 93 content_tag 'span', custom_field.name, :title => title, :class => css
94 94 end
95 95
96 96 # Return custom field label tag
97 97 def custom_field_label_tag(name, custom_value, options={})
98 98 required = options[:required] || custom_value.custom_field.is_required?
99 99 content = custom_field_name_tag custom_value.custom_field
100 100
101 101 content_tag "label", content +
102 102 (required ? " <span class=\"required\">*</span>".html_safe : ""),
103 103 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
104 104 end
105 105
106 106 # Return custom field tag with its label tag
107 107 def custom_field_tag_with_label(name, custom_value, options={})
108 108 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
109 109 end
110 110
111 111 # Returns the custom field tag for when bulk editing objects
112 112 def custom_field_tag_for_bulk_edit(prefix, custom_field, objects=nil, value='')
113 113 custom_field.format.bulk_edit_tag self,
114 114 custom_field_tag_id(prefix, custom_field),
115 115 custom_field_tag_name(prefix, custom_field),
116 116 custom_field,
117 117 objects,
118 118 value,
119 119 :class => "#{custom_field.field_format}_cf"
120 120 end
121 121
122 122 # Return a string used to display a custom value
123 123 def show_value(custom_value, html=true)
124 124 format_object(custom_value, html)
125 125 end
126 126
127 127 # Return a string used to display a custom value
128 128 def format_value(value, custom_field)
129 129 format_object(custom_field.format.formatted_value(self, custom_field, value, false), false)
130 130 end
131 131
132 132 # Return an array of custom field formats which can be used in select_tag
133 133 def custom_field_formats_for_select(custom_field)
134 134 Redmine::FieldFormat.as_select(custom_field.class.customized_class.name)
135 135 end
136 136
137 137 # Yields the given block for each custom field value of object that should be
138 138 # displayed, with the custom field and the formatted value as arguments
139 139 def render_custom_field_values(object, &block)
140 140 object.visible_custom_field_values.each do |custom_value|
141 141 formatted = show_value(custom_value)
142 142 if formatted.present?
143 143 yield custom_value.custom_field, formatted
144 144 end
145 145 end
146 146 end
147 147
148 148 # Renders the custom_values in api views
149 149 def render_api_custom_values(custom_values, api)
150 150 api.array :custom_fields do
151 151 custom_values.each do |custom_value|
152 152 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
153 153 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
154 154 api.custom_field attrs do
155 155 if custom_value.value.is_a?(Array)
156 156 api.array :value do
157 157 custom_value.value.each do |value|
158 158 api.value value unless value.blank?
159 159 end
160 160 end
161 161 else
162 162 api.value custom_value.value
163 163 end
164 164 end
165 165 end
166 166 end unless custom_values.empty?
167 167 end
168 168
169 169 def edit_tag_style_tag(form, options={})
170 170 select_options = [[l(:label_drop_down_list), ''], [l(:label_checkboxes), 'check_box']]
171 171 if options[:include_radio]
172 172 select_options << [l(:label_radio_buttons), 'radio']
173 173 end
174 174 form.select :edit_tag_style, select_options, :label => :label_display
175 175 end
176 176 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module DocumentsHelper
21 21 end
@@ -1,38 +1,38
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module EmailAddressesHelper
21 21
22 22 # Returns a link to enable or disable notifications for the address
23 23 def toggle_email_address_notify_link(address)
24 24 if address.notify?
25 25 link_to image_tag('email.png'),
26 26 user_email_address_path(address.user, address, :notify => '0'),
27 27 :method => :put,
28 28 :title => l(:label_disable_notifications),
29 29 :remote => true
30 30 else
31 31 link_to image_tag('email_disabled.png'),
32 32 user_email_address_path(address.user, address, :notify => '1'),
33 33 :method => :put,
34 34 :title => l(:label_enable_notifications),
35 35 :remote => true
36 36 end
37 37 end
38 38 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module EnumerationsHelper
21 21 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module GanttHelper
21 21
22 22 def gantt_zoom_link(gantt, in_or_out)
23 23 case in_or_out
24 24 when :in
25 25 if gantt.zoom < 4
26 26 link_to_content_update l(:text_zoom_in),
27 27 params.merge(gantt.params.merge(:zoom => (gantt.zoom + 1))),
28 28 :class => 'icon icon-zoom-in'
29 29 else
30 30 content_tag(:span, l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe
31 31 end
32 32
33 33 when :out
34 34 if gantt.zoom > 1
35 35 link_to_content_update l(:text_zoom_out),
36 36 params.merge(gantt.params.merge(:zoom => (gantt.zoom - 1))),
37 37 :class => 'icon icon-zoom-out'
38 38 else
39 39 content_tag(:span, l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
40 40 end
41 41 end
42 42 end
43 43 end
@@ -1,46 +1,46
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module GroupsHelper
21 21 def group_settings_tabs(group)
22 22 tabs = []
23 23 tabs << {:name => 'general', :partial => 'groups/general', :label => :label_general}
24 24 tabs << {:name => 'users', :partial => 'groups/users', :label => :label_user_plural} if group.givable?
25 25 tabs << {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
26 26 tabs
27 27 end
28 28
29 29 def render_principals_for_new_group_users(group, limit=100)
30 30 scope = User.active.sorted.not_in_group(group).like(params[:q])
31 31 principal_count = scope.count
32 32 principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
33 33 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
34 34
35 35 s = content_tag('div',
36 36 content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals'),
37 37 :class => 'objects-selection'
38 38 )
39 39
40 40 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
41 41 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
42 42 }
43 43
44 44 s + content_tag('span', links, :class => 'pagination')
45 45 end
46 46 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ImportsHelper
21 21 def options_for_mapping_select(import, field, options={})
22 22 tags = "".html_safe
23 23 blank_text = options[:required] ? "-- #{l(:actionview_instancetag_blank_option)} --" : "&nbsp;".html_safe
24 24 tags << content_tag('option', blank_text, :value => '')
25 25 tags << options_for_select(import.columns_options, import.mapping[field])
26 26 tags
27 27 end
28 28
29 29 def mapping_select_tag(import, field, options={})
30 30 name = "import_settings[mapping][#{field}]"
31 31 select_tag name, options_for_mapping_select(import, field, options)
32 32 end
33 33
34 34 # Returns the options for the date_format setting
35 35 def date_format_options
36 36 Import::DATE_FORMATS.map do |f|
37 37 format = f.gsub('%', '').gsub(/[dmY]/) do
38 38 {'d' => 'DD', 'm' => 'MM', 'Y' => 'YYYY'}[$&]
39 39 end
40 40 [format, f]
41 41 end
42 42 end
43 43 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssueCategoriesHelper
21 21 end
@@ -1,25 +1,25
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssueRelationsHelper
21 21 def collection_for_relation_type_select
22 22 values = IssueRelation::TYPES
23 23 values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
24 24 end
25 25 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssueStatusesHelper
21 21 end
@@ -1,516 +1,516
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssuesHelper
21 21 include ApplicationHelper
22 22 include Redmine::Export::PDF::IssuesPdfHelper
23 23
24 24 def issue_list(issues, &block)
25 25 ancestors = []
26 26 issues.each do |issue|
27 27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 28 ancestors.pop
29 29 end
30 30 yield issue, ancestors.size
31 31 ancestors << issue unless issue.leaf?
32 32 end
33 33 end
34 34
35 35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 36 previous_group, first = false, true
37 37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
38 38 h[column] = query.total_by_group_for(column)
39 39 h
40 40 end
41 41 issue_list(issues) do |issue, level|
42 42 group_name = group_count = nil
43 43 if query.grouped?
44 44 group = query.group_by_column.value(issue)
45 45 if first || group != previous_group
46 46 if group.blank? && group != false
47 47 group_name = "(#{l(:label_blank_value)})"
48 48 else
49 49 group_name = format_object(group)
50 50 end
51 51 group_name ||= ""
52 52 group_count = issue_count_by_group[group]
53 53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
54 54 end
55 55 end
56 56 yield issue, level, group_name, group_count, group_totals
57 57 previous_group, first = group, false
58 58 end
59 59 end
60 60
61 61 # Renders a HTML/CSS tooltip
62 62 #
63 63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
64 64 # that contains this method wrapped in a span with the class of "tip"
65 65 #
66 66 # <div class="tooltip"><%= link_to_issue(issue) %>
67 67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
68 68 # </div>
69 69 #
70 70 def render_issue_tooltip(issue)
71 71 @cached_label_status ||= l(:field_status)
72 72 @cached_label_start_date ||= l(:field_start_date)
73 73 @cached_label_due_date ||= l(:field_due_date)
74 74 @cached_label_assigned_to ||= l(:field_assigned_to)
75 75 @cached_label_priority ||= l(:field_priority)
76 76 @cached_label_project ||= l(:field_project)
77 77
78 78 link_to_issue(issue) + "<br /><br />".html_safe +
79 79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
80 80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
81 81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
82 82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
83 83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
84 84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
85 85 end
86 86
87 87 def issue_heading(issue)
88 88 h("#{issue.tracker} ##{issue.id}")
89 89 end
90 90
91 91 def render_issue_subject_with_tree(issue)
92 92 s = ''
93 93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
94 94 ancestors.each do |ancestor|
95 95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
96 96 end
97 97 s << '<div>'
98 98 subject = h(issue.subject)
99 99 if issue.is_private?
100 100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
101 101 end
102 102 s << content_tag('h3', subject)
103 103 s << '</div>' * (ancestors.size + 1)
104 104 s.html_safe
105 105 end
106 106
107 107 def render_descendants_tree(issue)
108 108 s = '<form><table class="list issues">'
109 109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker).sort_by(&:lft)) do |child, level|
110 110 css = "issue issue-#{child.id} hascontextmenu"
111 111 css << " idnt idnt-#{level}" if level > 0
112 112 s << content_tag('tr',
113 113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
114 114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
115 115 content_tag('td', h(child.status)) +
116 116 content_tag('td', link_to_user(child.assigned_to)) +
117 117 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio)),
118 118 :class => css)
119 119 end
120 120 s << '</table></form>'
121 121 s.html_safe
122 122 end
123 123
124 124 def issue_estimated_hours_details(issue)
125 125 if issue.total_estimated_hours.present?
126 126 if issue.total_estimated_hours == issue.estimated_hours
127 127 l_hours_short(issue.estimated_hours)
128 128 else
129 129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
130 130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
131 131 s.html_safe
132 132 end
133 133 end
134 134 end
135 135
136 136 def issue_spent_hours_details(issue)
137 137 if issue.total_spent_hours > 0
138 138 if issue.total_spent_hours == issue.spent_hours
139 139 link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
140 140 else
141 141 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
142 142 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), issue_time_entries_path(issue)})"
143 143 s.html_safe
144 144 end
145 145 end
146 146 end
147 147
148 148 # Returns an array of error messages for bulk edited issues
149 149 def bulk_edit_error_messages(issues)
150 150 messages = {}
151 151 issues.each do |issue|
152 152 issue.errors.full_messages.each do |message|
153 153 messages[message] ||= []
154 154 messages[message] << issue
155 155 end
156 156 end
157 157 messages.map { |message, issues|
158 158 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
159 159 }
160 160 end
161 161
162 162 # Returns a link for adding a new subtask to the given issue
163 163 def link_to_new_subtask(issue)
164 164 attrs = {
165 165 :tracker_id => issue.tracker,
166 166 :parent_issue_id => issue
167 167 }
168 168 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
169 169 end
170 170
171 171 class IssueFieldsRows
172 172 include ActionView::Helpers::TagHelper
173 173
174 174 def initialize
175 175 @left = []
176 176 @right = []
177 177 end
178 178
179 179 def left(*args)
180 180 args.any? ? @left << cells(*args) : @left
181 181 end
182 182
183 183 def right(*args)
184 184 args.any? ? @right << cells(*args) : @right
185 185 end
186 186
187 187 def size
188 188 @left.size > @right.size ? @left.size : @right.size
189 189 end
190 190
191 191 def to_html
192 192 content =
193 193 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
194 194 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
195 195
196 196 content_tag('div', content, :class => 'splitcontent')
197 197 end
198 198
199 199 def cells(label, text, options={})
200 200 options[:class] = [options[:class] || "", 'attribute'].join(' ')
201 201 content_tag 'div',
202 202 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
203 203 options
204 204 end
205 205 end
206 206
207 207 def issue_fields_rows
208 208 r = IssueFieldsRows.new
209 209 yield r
210 210 r.to_html
211 211 end
212 212
213 213 def render_custom_fields_rows(issue)
214 214 values = issue.visible_custom_field_values
215 215 return if values.empty?
216 216 half = (values.size / 2.0).ceil
217 217 issue_fields_rows do |rows|
218 218 values.each_with_index do |value, i|
219 219 css = "cf_#{value.custom_field.id}"
220 220 m = (i < half ? :left : :right)
221 221 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
222 222 end
223 223 end
224 224 end
225 225
226 226 # Returns the path for updating the issue form
227 227 # with project as the current project
228 228 def update_issue_form_path(project, issue)
229 229 options = {:format => 'js'}
230 230 if issue.new_record?
231 231 if project
232 232 new_project_issue_path(project, options)
233 233 else
234 234 new_issue_path(options)
235 235 end
236 236 else
237 237 edit_issue_path(issue, options)
238 238 end
239 239 end
240 240
241 241 # Returns the number of descendants for an array of issues
242 242 def issues_descendant_count(issues)
243 243 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
244 244 ids -= issues.map(&:id)
245 245 ids.size
246 246 end
247 247
248 248 def issues_destroy_confirmation_message(issues)
249 249 issues = [issues] unless issues.is_a?(Array)
250 250 message = l(:text_issues_destroy_confirmation)
251 251
252 252 descendant_count = issues_descendant_count(issues)
253 253 if descendant_count > 0
254 254 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
255 255 end
256 256 message
257 257 end
258 258
259 259 # Returns an array of users that are proposed as watchers
260 260 # on the new issue form
261 261 def users_for_new_issue_watchers(issue)
262 262 users = issue.watcher_users
263 263 if issue.project.users.count <= 20
264 264 users = (users + issue.project.users.sort).uniq
265 265 end
266 266 users
267 267 end
268 268
269 269 def sidebar_queries
270 270 unless @sidebar_queries
271 271 @sidebar_queries = IssueQuery.visible.
272 272 order("#{Query.table_name}.name ASC").
273 273 # Project specific queries and global queries
274 274 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
275 275 to_a
276 276 end
277 277 @sidebar_queries
278 278 end
279 279
280 280 def query_links(title, queries)
281 281 return '' if queries.empty?
282 282 # links to #index on issues/show
283 283 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
284 284
285 285 content_tag('h3', title) + "\n" +
286 286 content_tag('ul',
287 287 queries.collect {|query|
288 288 css = 'query'
289 289 css << ' selected' if query == @query
290 290 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
291 291 }.join("\n").html_safe,
292 292 :class => 'queries'
293 293 ) + "\n"
294 294 end
295 295
296 296 def render_sidebar_queries
297 297 out = ''.html_safe
298 298 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
299 299 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
300 300 out
301 301 end
302 302
303 303 def email_issue_attributes(issue, user)
304 304 items = []
305 305 %w(author status priority assigned_to category fixed_version).each do |attribute|
306 306 unless issue.disabled_core_fields.include?(attribute+"_id")
307 307 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
308 308 end
309 309 end
310 310 issue.visible_custom_field_values(user).each do |value|
311 311 items << "#{value.custom_field.name}: #{show_value(value, false)}"
312 312 end
313 313 items
314 314 end
315 315
316 316 def render_email_issue_attributes(issue, user, html=false)
317 317 items = email_issue_attributes(issue, user)
318 318 if html
319 319 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
320 320 else
321 321 items.map{|s| "* #{s}"}.join("\n")
322 322 end
323 323 end
324 324
325 325 # Returns the textual representation of a journal details
326 326 # as an array of strings
327 327 def details_to_strings(details, no_html=false, options={})
328 328 options[:only_path] = (options[:only_path] == false ? false : true)
329 329 strings = []
330 330 values_by_field = {}
331 331 details.each do |detail|
332 332 if detail.property == 'cf'
333 333 field = detail.custom_field
334 334 if field && field.multiple?
335 335 values_by_field[field] ||= {:added => [], :deleted => []}
336 336 if detail.old_value
337 337 values_by_field[field][:deleted] << detail.old_value
338 338 end
339 339 if detail.value
340 340 values_by_field[field][:added] << detail.value
341 341 end
342 342 next
343 343 end
344 344 end
345 345 strings << show_detail(detail, no_html, options)
346 346 end
347 347 if values_by_field.present?
348 348 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
349 349 values_by_field.each do |field, changes|
350 350 if changes[:added].any?
351 351 detail = multiple_values_detail.new('cf', field.id.to_s, field)
352 352 detail.value = changes[:added]
353 353 strings << show_detail(detail, no_html, options)
354 354 end
355 355 if changes[:deleted].any?
356 356 detail = multiple_values_detail.new('cf', field.id.to_s, field)
357 357 detail.old_value = changes[:deleted]
358 358 strings << show_detail(detail, no_html, options)
359 359 end
360 360 end
361 361 end
362 362 strings
363 363 end
364 364
365 365 # Returns the textual representation of a single journal detail
366 366 def show_detail(detail, no_html=false, options={})
367 367 multiple = false
368 368 show_diff = false
369 369
370 370 case detail.property
371 371 when 'attr'
372 372 field = detail.prop_key.to_s.gsub(/\_id$/, "")
373 373 label = l(("field_" + field).to_sym)
374 374 case detail.prop_key
375 375 when 'due_date', 'start_date'
376 376 value = format_date(detail.value.to_date) if detail.value
377 377 old_value = format_date(detail.old_value.to_date) if detail.old_value
378 378
379 379 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
380 380 'priority_id', 'category_id', 'fixed_version_id'
381 381 value = find_name_by_reflection(field, detail.value)
382 382 old_value = find_name_by_reflection(field, detail.old_value)
383 383
384 384 when 'estimated_hours'
385 385 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
386 386 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
387 387
388 388 when 'parent_id'
389 389 label = l(:field_parent_issue)
390 390 value = "##{detail.value}" unless detail.value.blank?
391 391 old_value = "##{detail.old_value}" unless detail.old_value.blank?
392 392
393 393 when 'is_private'
394 394 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
395 395 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
396 396
397 397 when 'description'
398 398 show_diff = true
399 399 end
400 400 when 'cf'
401 401 custom_field = detail.custom_field
402 402 if custom_field
403 403 label = custom_field.name
404 404 if custom_field.format.class.change_as_diff
405 405 show_diff = true
406 406 else
407 407 multiple = custom_field.multiple?
408 408 value = format_value(detail.value, custom_field) if detail.value
409 409 old_value = format_value(detail.old_value, custom_field) if detail.old_value
410 410 end
411 411 end
412 412 when 'attachment'
413 413 label = l(:label_attachment)
414 414 when 'relation'
415 415 if detail.value && !detail.old_value
416 416 rel_issue = Issue.visible.find_by_id(detail.value)
417 417 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
418 418 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
419 419 elsif detail.old_value && !detail.value
420 420 rel_issue = Issue.visible.find_by_id(detail.old_value)
421 421 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
422 422 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
423 423 end
424 424 relation_type = IssueRelation::TYPES[detail.prop_key]
425 425 label = l(relation_type[:name]) if relation_type
426 426 end
427 427 call_hook(:helper_issues_show_detail_after_setting,
428 428 {:detail => detail, :label => label, :value => value, :old_value => old_value })
429 429
430 430 label ||= detail.prop_key
431 431 value ||= detail.value
432 432 old_value ||= detail.old_value
433 433
434 434 unless no_html
435 435 label = content_tag('strong', label)
436 436 old_value = content_tag("i", h(old_value)) if detail.old_value
437 437 if detail.old_value && detail.value.blank? && detail.property != 'relation'
438 438 old_value = content_tag("del", old_value)
439 439 end
440 440 if detail.property == 'attachment' && value.present? &&
441 441 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
442 442 # Link to the attachment if it has not been removed
443 443 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
444 444 if options[:only_path] != false && atta.is_text?
445 445 value += link_to(
446 446 image_tag('magnifier.png'),
447 447 :controller => 'attachments', :action => 'show',
448 448 :id => atta, :filename => atta.filename
449 449 )
450 450 end
451 451 else
452 452 value = content_tag("i", h(value)) if value
453 453 end
454 454 end
455 455
456 456 if show_diff
457 457 s = l(:text_journal_changed_no_detail, :label => label)
458 458 unless no_html
459 459 diff_link = link_to 'diff',
460 460 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
461 461 :detail_id => detail.id, :only_path => options[:only_path]},
462 462 :title => l(:label_view_diff)
463 463 s << " (#{ diff_link })"
464 464 end
465 465 s.html_safe
466 466 elsif detail.value.present?
467 467 case detail.property
468 468 when 'attr', 'cf'
469 469 if detail.old_value.present?
470 470 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
471 471 elsif multiple
472 472 l(:text_journal_added, :label => label, :value => value).html_safe
473 473 else
474 474 l(:text_journal_set_to, :label => label, :value => value).html_safe
475 475 end
476 476 when 'attachment', 'relation'
477 477 l(:text_journal_added, :label => label, :value => value).html_safe
478 478 end
479 479 else
480 480 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
481 481 end
482 482 end
483 483
484 484 # Find the name of an associated record stored in the field attribute
485 485 def find_name_by_reflection(field, id)
486 486 unless id.present?
487 487 return nil
488 488 end
489 489 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
490 490 association = Issue.reflect_on_association(key.first.to_sym)
491 491 name = nil
492 492 if association
493 493 record = association.klass.find_by_id(key.last)
494 494 if record
495 495 name = record.name.force_encoding('UTF-8')
496 496 end
497 497 end
498 498 hash[key] = name
499 499 end
500 500 @detail_value_name_by_reflection[[field, id]]
501 501 end
502 502
503 503 # Renders issue children recursively
504 504 def render_api_issue_children(issue, api)
505 505 return if issue.leaf?
506 506 api.array :children do
507 507 issue.children.each do |child|
508 508 api.issue(:id => child.id) do
509 509 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
510 510 api.subject child.subject
511 511 render_api_issue_children(child, api)
512 512 end
513 513 end
514 514 end
515 515 end
516 516 end
@@ -1,53 +1,53
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module JournalsHelper
21 21
22 22 # Returns the attachments of a journal that are displayed as thumbnails
23 23 def journal_thumbnail_attachments(journal)
24 24 ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
25 25 ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
26 26 end
27 27
28 28 def render_notes(issue, journal, options={})
29 29 content = ''
30 30 editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
31 31 links = []
32 32 if !journal.notes.blank?
33 33 links << link_to(image_tag('comment.png'),
34 34 {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
35 35 :remote => true,
36 36 :method => 'post',
37 37 :title => l(:button_quote)) if options[:reply_links]
38 38 links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
39 39 { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
40 40 :title => l(:button_edit)) if editable
41 41 end
42 42 content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
43 43 content << textilizable(journal, :notes)
44 44 css_classes = "wiki"
45 45 css_classes << " editable" if editable
46 46 content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
47 47 end
48 48
49 49 def link_to_in_place_notes_editor(text, field_id, url, options={})
50 50 onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
51 51 link_to text, '#', options.merge(:onclick => onclick)
52 52 end
53 53 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MailHandlerHelper
21 21 end
@@ -1,38 +1,38
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MembersHelper
21 21 def render_principals_for_new_members(project, limit=100)
22 22 scope = Principal.active.visible.sorted.not_member_of(project).like(params[:q])
23 23 principal_count = scope.count
24 24 principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
25 25 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
26 26
27 27 s = content_tag('div',
28 28 content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals'),
29 29 :class => 'objects-selection'
30 30 )
31 31
32 32 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
33 33 link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
34 34 }
35 35
36 36 s + content_tag('span', links, :class => 'pagination')
37 37 end
38 38 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MessagesHelper
21 21 end
@@ -1,75 +1,75
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MyHelper
21 21 def calendar_items(startdt, enddt)
22 22 Issue.visible.
23 23 where(:project_id => User.current.projects.map(&:id)).
24 24 where("(start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)", startdt, enddt, startdt, enddt).
25 25 includes(:project, :tracker, :priority, :assigned_to).
26 26 references(:project, :tracker, :priority, :assigned_to).
27 27 to_a
28 28 end
29 29
30 30 def documents_items
31 31 Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).to_a
32 32 end
33 33
34 34 def issuesassignedtome_items
35 35 Issue.visible.open.
36 36 assigned_to(User.current).
37 37 limit(10).
38 38 includes(:status, :project, :tracker, :priority).
39 39 references(:status, :project, :tracker, :priority).
40 40 order("#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC")
41 41 end
42 42
43 43 def issuesreportedbyme_items
44 44 Issue.visible.
45 45 where(:author_id => User.current.id).
46 46 limit(10).
47 47 includes(:status, :project, :tracker).
48 48 references(:status, :project, :tracker).
49 49 order("#{Issue.table_name}.updated_on DESC")
50 50 end
51 51
52 52 def issueswatched_items
53 53 Issue.visible.on_active_project.watched_by(User.current.id).recently_updated.limit(10)
54 54 end
55 55
56 56 def news_items
57 57 News.visible.
58 58 where(:project_id => User.current.projects.map(&:id)).
59 59 limit(10).
60 60 includes(:project, :author).
61 61 references(:project, :author).
62 62 order("#{News.table_name}.created_on DESC").
63 63 to_a
64 64 end
65 65
66 66 def timelog_items
67 67 TimeEntry.
68 68 where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, Date.today - 6, Date.today).
69 69 joins(:activity, :project).
70 70 references(:issue => [:tracker, :status]).
71 71 includes(:issue => [:tracker, :status]).
72 72 order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC").
73 73 to_a
74 74 end
75 75 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module NewsHelper
21 21 end
@@ -1,56 +1,56
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module PrincipalMembershipsHelper
21 21 def render_principal_memberships(principal)
22 22 render :partial => 'principal_memberships/index', :locals => {:principal => principal}
23 23 end
24 24
25 25 def call_table_header_hook(principal)
26 26 if principal.is_a?(Group)
27 27 call_hook :view_groups_memberships_table_header, :group => principal
28 28 else
29 29 call_hook :view_users_memberships_table_header, :user => principal
30 30 end
31 31 end
32 32
33 33 def call_table_row_hook(principal, membership)
34 34 if principal.is_a?(Group)
35 35 call_hook :view_groups_memberships_table_row, :group => principal, :membership => membership
36 36 else
37 37 call_hook :view_users_memberships_table_row, :user => principal, :membership => membership
38 38 end
39 39 end
40 40
41 41 def new_principal_membership_path(principal, *args)
42 42 if principal.is_a?(Group)
43 43 new_group_membership_path(principal, *args)
44 44 else
45 45 new_user_membership_path(principal, *args)
46 46 end
47 47 end
48 48
49 49 def principal_membership_path(principal, membership, *args)
50 50 if principal.is_a?(Group)
51 51 group_membership_path(principal, membership, *args)
52 52 else
53 53 user_membership_path(principal, membership, *args)
54 54 end
55 55 end
56 56 end
@@ -1,123 +1,123
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ProjectsHelper
21 21 def project_settings_tabs
22 22 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
23 23 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
24 24 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
25 25 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
26 26 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
27 27 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
28 28 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
29 29 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
30 30 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
31 31 ]
32 32 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
33 33 end
34 34
35 35 def parent_project_select_tag(project)
36 36 selected = project.parent
37 37 # retrieve the requested parent project
38 38 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
39 39 if parent_id
40 40 selected = (parent_id.blank? ? nil : Project.find(parent_id))
41 41 end
42 42
43 43 options = ''
44 44 options << "<option value=''>&nbsp;</option>" if project.allowed_parents.include?(nil)
45 45 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
46 46 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
47 47 end
48 48
49 49 def render_project_action_links
50 50 links = []
51 51 if User.current.allowed_to?(:add_project, nil, :global => true)
52 52 links << link_to(l(:label_project_new), new_project_path, :class => 'icon icon-add')
53 53 end
54 54 if User.current.allowed_to?(:view_issues, nil, :global => true)
55 55 links << link_to(l(:label_issue_view_all), issues_path)
56 56 end
57 57 if User.current.allowed_to?(:view_time_entries, nil, :global => true)
58 58 links << link_to(l(:label_overall_spent_time), time_entries_path)
59 59 end
60 60 links << link_to(l(:label_overall_activity), activity_path)
61 61 links.join(" | ").html_safe
62 62 end
63 63
64 64 # Renders the projects index
65 65 def render_project_hierarchy(projects)
66 66 render_project_nested_lists(projects) do |project|
67 67 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
68 68 if project.description.present?
69 69 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
70 70 end
71 71 s
72 72 end
73 73 end
74 74
75 75 # Returns a set of options for a select field, grouped by project.
76 76 def version_options_for_select(versions, selected=nil)
77 77 grouped = Hash.new {|h,k| h[k] = []}
78 78 versions.each do |version|
79 79 grouped[version.project.name] << [version.name, version.id]
80 80 end
81 81
82 82 selected = selected.is_a?(Version) ? selected.id : selected
83 83 if grouped.keys.size > 1
84 84 grouped_options_for_select(grouped, selected)
85 85 else
86 86 options_for_select((grouped.values.first || []), selected)
87 87 end
88 88 end
89 89
90 90 def project_default_version_options(project)
91 91 versions = project.shared_versions.open.to_a
92 92 if project.default_version && !versions.include?(project.default_version)
93 93 versions << project.default_version
94 94 end
95 95 version_options_for_select(versions, project.default_version)
96 96 end
97 97
98 98 def format_version_sharing(sharing)
99 99 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
100 100 l("label_version_sharing_#{sharing}")
101 101 end
102 102
103 103 def render_api_includes(project, api)
104 104 api.array :trackers do
105 105 project.trackers.each do |tracker|
106 106 api.tracker(:id => tracker.id, :name => tracker.name)
107 107 end
108 108 end if include_in_api_response?('trackers')
109 109
110 110 api.array :issue_categories do
111 111 project.issue_categories.each do |category|
112 112 api.issue_category(:id => category.id, :name => category.name)
113 113 end
114 114 end if include_in_api_response?('issue_categories')
115 115
116 116 api.array :enabled_modules do
117 117 project.enabled_modules.each do |enabled_module|
118 118 api.enabled_module(:id => enabled_module.id, :name => enabled_module.name)
119 119 end
120 120 end if include_in_api_response?('enabled_modules')
121 121
122 122 end
123 123 end
@@ -1,276 +1,276
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 ungrouped = []
25 25 grouped = {}
26 26 query.available_filters.map do |field, field_options|
27 27 if [:tree, :relation].include?(field_options[:type])
28 28 group = :label_related_issues
29 29 elsif field =~ /^(.+)\./
30 30 # association filters
31 31 group = "field_#{$1}"
32 32 elsif %w(member_of_group assigned_to_role).include?(field)
33 33 group = :field_assigned_to
34 34 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 35 group = :label_date
36 36 end
37 37 if group
38 38 (grouped[group] ||= []) << [field_options[:name], field]
39 39 else
40 40 ungrouped << [field_options[:name], field]
41 41 end
42 42 end
43 43 # Don't group dates if there's only one (eg. time entries filters)
44 44 if grouped[:label_date].try(:size) == 1
45 45 ungrouped << grouped.delete(:label_date).first
46 46 end
47 47 s = options_for_select([[]] + ungrouped)
48 48 if grouped.present?
49 49 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 50 s << grouped_options_for_select(localized_grouped)
51 51 end
52 52 s
53 53 end
54 54
55 55 def query_filters_hidden_tags(query)
56 56 tags = ''.html_safe
57 57 query.filters.each do |field, options|
58 58 tags << hidden_field_tag("f[]", field, :id => nil)
59 59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 60 options[:values].each do |value|
61 61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 62 end
63 63 end
64 64 tags
65 65 end
66 66
67 67 def query_columns_hidden_tags(query)
68 68 tags = ''.html_safe
69 69 query.columns.each do |column|
70 70 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 71 end
72 72 tags
73 73 end
74 74
75 75 def query_hidden_tags(query)
76 76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 77 end
78 78
79 79 def available_block_columns_tags(query)
80 80 tags = ''.html_safe
81 81 query.available_block_columns.each do |column|
82 82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 83 end
84 84 tags
85 85 end
86 86
87 87 def available_totalable_columns_tags(query)
88 88 tags = ''.html_safe
89 89 query.available_totalable_columns.each do |column|
90 90 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
91 91 end
92 92 tags
93 93 end
94 94
95 95 def query_available_inline_columns_options(query)
96 96 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
97 97 end
98 98
99 99 def query_selected_inline_columns_options(query)
100 100 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
101 101 end
102 102
103 103 def render_query_columns_selection(query, options={})
104 104 tag_name = (options[:name] || 'c') + '[]'
105 105 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
106 106 end
107 107
108 108 def render_query_totals(query)
109 109 return unless query.totalable_columns.present?
110 110 totals = query.totalable_columns.map do |column|
111 111 total_tag(column, query.total_for(column))
112 112 end
113 113 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
114 114 end
115 115
116 116 def total_tag(column, value)
117 117 label = content_tag('span', "#{column.caption}:")
118 118 value = content_tag('span', format_object(value), :class => 'value')
119 119 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
120 120 end
121 121
122 122 def column_header(column)
123 123 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
124 124 :default_order => column.default_order) :
125 125 content_tag('th', h(column.caption))
126 126 end
127 127
128 128 def column_content(column, issue)
129 129 value = column.value_object(issue)
130 130 if value.is_a?(Array)
131 131 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
132 132 else
133 133 column_value(column, issue, value)
134 134 end
135 135 end
136 136
137 137 def column_value(column, issue, value)
138 138 case column.name
139 139 when :id
140 140 link_to value, issue_path(issue)
141 141 when :subject
142 142 link_to value, issue_path(issue)
143 143 when :parent
144 144 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
145 145 when :description
146 146 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
147 147 when :done_ratio
148 148 progress_bar(value)
149 149 when :relations
150 150 content_tag('span',
151 151 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
152 152 :class => value.css_classes_for(issue))
153 153 else
154 154 format_object(value)
155 155 end
156 156 end
157 157
158 158 def csv_content(column, issue)
159 159 value = column.value_object(issue)
160 160 if value.is_a?(Array)
161 161 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
162 162 else
163 163 csv_value(column, issue, value)
164 164 end
165 165 end
166 166
167 167 def csv_value(column, object, value)
168 168 format_object(value, false) do |value|
169 169 case value.class.name
170 170 when 'Float'
171 171 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
172 172 when 'IssueRelation'
173 173 value.to_s(object)
174 174 when 'Issue'
175 175 if object.is_a?(TimeEntry)
176 176 "#{value.tracker} ##{value.id}: #{value.subject}"
177 177 else
178 178 value.id
179 179 end
180 180 else
181 181 value
182 182 end
183 183 end
184 184 end
185 185
186 186 def query_to_csv(items, query, options={})
187 187 options ||= {}
188 188 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
189 189 query.available_block_columns.each do |column|
190 190 if options[column.name].present?
191 191 columns << column
192 192 end
193 193 end
194 194
195 195 Redmine::Export::CSV.generate do |csv|
196 196 # csv header fields
197 197 csv << columns.map {|c| c.caption.to_s}
198 198 # csv lines
199 199 items.each do |item|
200 200 csv << columns.map {|c| csv_content(c, item)}
201 201 end
202 202 end
203 203 end
204 204
205 205 # Retrieve query from session or build a new query
206 206 def retrieve_query
207 207 if !params[:query_id].blank?
208 208 cond = "project_id IS NULL"
209 209 cond << " OR project_id = #{@project.id}" if @project
210 210 @query = IssueQuery.where(cond).find(params[:query_id])
211 211 raise ::Unauthorized unless @query.visible?
212 212 @query.project = @project
213 213 session[:query] = {:id => @query.id, :project_id => @query.project_id}
214 214 sort_clear
215 215 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
216 216 # Give it a name, required to be valid
217 217 @query = IssueQuery.new(:name => "_")
218 218 @query.project = @project
219 219 @query.build_from_params(params)
220 220 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names}
221 221 else
222 222 # retrieve from session
223 223 @query = nil
224 224 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
225 225 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
226 226 @query.project = @project
227 227 end
228 228 end
229 229
230 230 def retrieve_query_from_session
231 231 if session[:query]
232 232 if session[:query][:id]
233 233 @query = IssueQuery.find_by_id(session[:query][:id])
234 234 return unless @query
235 235 else
236 236 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
237 237 end
238 238 if session[:query].has_key?(:project_id)
239 239 @query.project_id = session[:query][:project_id]
240 240 else
241 241 @query.project = @project
242 242 end
243 243 @query
244 244 end
245 245 end
246 246
247 247 # Returns the query definition as hidden field tags
248 248 def query_as_hidden_field_tags(query)
249 249 tags = hidden_field_tag("set_filter", "1", :id => nil)
250 250
251 251 if query.filters.present?
252 252 query.filters.each do |field, filter|
253 253 tags << hidden_field_tag("f[]", field, :id => nil)
254 254 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
255 255 filter[:values].each do |value|
256 256 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
257 257 end
258 258 end
259 259 end
260 260 if query.column_names.present?
261 261 query.column_names.each do |name|
262 262 tags << hidden_field_tag("c[]", name, :id => nil)
263 263 end
264 264 end
265 265 if query.totalable_names.present?
266 266 query.totalable_names.each do |name|
267 267 tags << hidden_field_tag("t[]", name, :id => nil)
268 268 end
269 269 end
270 270 if query.group_by.present?
271 271 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
272 272 end
273 273
274 274 tags
275 275 end
276 276 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ReportsHelper
21 21
22 22 def aggregate(data, criteria)
23 23 a = 0
24 24 data.each { |row|
25 25 match = 1
26 26 criteria.each { |k, v|
27 27 match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && (v == 0 ? ['f', false] : ['t', true]).include?(row[k]))
28 28 } unless criteria.nil?
29 29 a = a + row["total"].to_i if match == 1
30 30 } unless data.nil?
31 31 a
32 32 end
33 33
34 34 def aggregate_link(data, criteria, *args)
35 35 a = aggregate data, criteria
36 36 a > 0 ? link_to(h(a), *args) : '-'
37 37 end
38 38
39 39 def aggregate_path(project, field, row, options={})
40 40 parameters = {:set_filter => 1, :subproject_id => '!*', field => row.id}.merge(options)
41 41 project_issues_path(row.is_a?(Project) ? row : project, parameters)
42 42 end
43 43 end
@@ -1,310 +1,310
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RepositoriesHelper
21 21 def format_revision(revision)
22 22 if revision.respond_to? :format_identifier
23 23 revision.format_identifier
24 24 else
25 25 revision.to_s
26 26 end
27 27 end
28 28
29 29 def truncate_at_line_break(text, length = 255)
30 30 if text
31 31 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
32 32 end
33 33 end
34 34
35 35 def render_properties(properties)
36 36 unless properties.nil? || properties.empty?
37 37 content = ''
38 38 properties.keys.sort.each do |property|
39 39 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
40 40 end
41 41 content_tag('ul', content.html_safe, :class => 'properties')
42 42 end
43 43 end
44 44
45 45 def render_changeset_changes
46 46 changes = @changeset.filechanges.limit(1000).reorder('path').collect do |change|
47 47 case change.action
48 48 when 'A'
49 49 # Detects moved/copied files
50 50 if !change.from_path.blank?
51 51 change.action =
52 52 @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
53 53 end
54 54 change
55 55 when 'D'
56 56 @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
57 57 else
58 58 change
59 59 end
60 60 end.compact
61 61
62 62 tree = { }
63 63 changes.each do |change|
64 64 p = tree
65 65 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
66 66 path = ''
67 67 dirs.each do |dir|
68 68 path += '/' + dir
69 69 p[:s] ||= {}
70 70 p = p[:s]
71 71 p[path] ||= {}
72 72 p = p[path]
73 73 end
74 74 p[:c] = change
75 75 end
76 76 render_changes_tree(tree[:s])
77 77 end
78 78
79 79 def render_changes_tree(tree)
80 80 return '' if tree.nil?
81 81 output = ''
82 82 output << '<ul>'
83 83 tree.keys.sort.each do |file|
84 84 style = 'change'
85 85 text = File.basename(h(file))
86 86 if s = tree[file][:s]
87 87 style << ' folder'
88 88 path_param = to_path_param(@repository.relative_path(file))
89 89 text = link_to(h(text), :controller => 'repositories',
90 90 :action => 'show',
91 91 :id => @project,
92 92 :repository_id => @repository.identifier_param,
93 93 :path => path_param,
94 94 :rev => @changeset.identifier)
95 95 output << "<li class='#{style}'>#{text}"
96 96 output << render_changes_tree(s)
97 97 output << "</li>"
98 98 elsif c = tree[file][:c]
99 99 style << " change-#{c.action}"
100 100 path_param = to_path_param(@repository.relative_path(c.path))
101 101 text = link_to(h(text), :controller => 'repositories',
102 102 :action => 'entry',
103 103 :id => @project,
104 104 :repository_id => @repository.identifier_param,
105 105 :path => path_param,
106 106 :rev => @changeset.identifier) unless c.action == 'D'
107 107 text << " - #{h(c.revision)}" unless c.revision.blank?
108 108 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
109 109 :action => 'diff',
110 110 :id => @project,
111 111 :repository_id => @repository.identifier_param,
112 112 :path => path_param,
113 113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 115 output << "<li class='#{style}'>#{text}</li>"
116 116 end
117 117 end
118 118 output << '</ul>'
119 119 output.html_safe
120 120 end
121 121
122 122 def repository_field_tags(form, repository)
123 123 method = repository.class.name.demodulize.underscore + "_field_tags"
124 124 if repository.is_a?(Repository) &&
125 125 respond_to?(method) && method != 'repository_field_tags'
126 126 send(method, form, repository)
127 127 end
128 128 end
129 129
130 130 def scm_select_tag(repository)
131 131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 132 Redmine::Scm::Base.all.each do |scm|
133 133 if Setting.enabled_scm.include?(scm) ||
134 134 (repository && repository.class.name.demodulize == scm)
135 135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 136 end
137 137 end
138 138 select_tag('repository_scm',
139 139 options_for_select(scm_options, repository.class.name.demodulize),
140 140 :disabled => (repository && !repository.new_record?),
141 141 :data => {:remote => true, :method => 'get'})
142 142 end
143 143
144 144 def with_leading_slash(path)
145 145 path.to_s.starts_with?('/') ? path : "/#{path}"
146 146 end
147 147
148 148 def subversion_field_tags(form, repository)
149 149 content_tag('p', form.text_field(:url, :size => 60, :required => true,
150 150 :disabled => !repository.safe_attribute?('url')) +
151 151 scm_path_info_tag(repository)) +
152 152 content_tag('p', form.text_field(:login, :size => 30)) +
153 153 content_tag('p', form.password_field(
154 154 :password, :size => 30, :name => 'ignore',
155 155 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
156 156 :onfocus => "this.value=''; this.name='repository[password]';",
157 157 :onchange => "this.name='repository[password]';"))
158 158 end
159 159
160 160 def darcs_field_tags(form, repository)
161 161 content_tag('p', form.text_field(
162 162 :url, :label => l(:field_path_to_repository),
163 163 :size => 60, :required => true,
164 164 :disabled => !repository.safe_attribute?('url')) +
165 165 scm_path_info_tag(repository)) +
166 166 scm_log_encoding_tag(form, repository)
167 167 end
168 168
169 169 def mercurial_field_tags(form, repository)
170 170 content_tag('p', form.text_field(
171 171 :url, :label => l(:field_path_to_repository),
172 172 :size => 60, :required => true,
173 173 :disabled => !repository.safe_attribute?('url')
174 174 ) +
175 175 scm_path_info_tag(repository)) +
176 176 scm_path_encoding_tag(form, repository)
177 177 end
178 178
179 179 def git_field_tags(form, repository)
180 180 content_tag('p', form.text_field(
181 181 :url, :label => l(:field_path_to_repository),
182 182 :size => 60, :required => true,
183 183 :disabled => !repository.safe_attribute?('url')
184 184 ) +
185 185 scm_path_info_tag(repository)) +
186 186 scm_path_encoding_tag(form, repository) +
187 187 content_tag('p', form.check_box(
188 188 :extra_report_last_commit,
189 189 :label => l(:label_git_report_last_commit)
190 190 ))
191 191 end
192 192
193 193 def cvs_field_tags(form, repository)
194 194 content_tag('p', form.text_field(
195 195 :root_url,
196 196 :label => l(:field_cvsroot),
197 197 :size => 60, :required => true,
198 198 :disabled => !repository.safe_attribute?('root_url')) +
199 199 scm_path_info_tag(repository)) +
200 200 content_tag('p', form.text_field(
201 201 :url,
202 202 :label => l(:field_cvs_module),
203 203 :size => 30, :required => true,
204 204 :disabled => !repository.safe_attribute?('url'))) +
205 205 scm_log_encoding_tag(form, repository) +
206 206 scm_path_encoding_tag(form, repository)
207 207 end
208 208
209 209 def bazaar_field_tags(form, repository)
210 210 content_tag('p', form.text_field(
211 211 :url, :label => l(:field_path_to_repository),
212 212 :size => 60, :required => true,
213 213 :disabled => !repository.safe_attribute?('url')) +
214 214 scm_path_info_tag(repository)) +
215 215 scm_log_encoding_tag(form, repository)
216 216 end
217 217
218 218 def filesystem_field_tags(form, repository)
219 219 content_tag('p', form.text_field(
220 220 :url, :label => l(:field_root_directory),
221 221 :size => 60, :required => true,
222 222 :disabled => !repository.safe_attribute?('url')) +
223 223 scm_path_info_tag(repository)) +
224 224 scm_path_encoding_tag(form, repository)
225 225 end
226 226
227 227 def scm_path_info_tag(repository)
228 228 text = scm_path_info(repository)
229 229 if text.present?
230 230 content_tag('em', text, :class => 'info')
231 231 else
232 232 ''
233 233 end
234 234 end
235 235
236 236 def scm_path_info(repository)
237 237 scm_name = repository.scm_name.to_s.downcase
238 238
239 239 info_from_config = Redmine::Configuration["scm_#{scm_name}_path_info"].presence
240 240 return info_from_config.html_safe if info_from_config
241 241
242 242 l("text_#{scm_name}_repository_note", :default => '')
243 243 end
244 244
245 245 def scm_log_encoding_tag(form, repository)
246 246 select = form.select(
247 247 :log_encoding,
248 248 [nil] + Setting::ENCODINGS,
249 249 :label => l(:field_commit_logs_encoding),
250 250 :required => true
251 251 )
252 252 content_tag('p', select)
253 253 end
254 254
255 255 def scm_path_encoding_tag(form, repository)
256 256 select = form.select(
257 257 :path_encoding,
258 258 [nil] + Setting::ENCODINGS,
259 259 :label => l(:field_scm_path_encoding)
260 260 )
261 261 content_tag('p', select + content_tag('em', l(:text_scm_path_encoding_note), :class => 'info'))
262 262 end
263 263
264 264 def index_commits(commits, heads)
265 265 return nil if commits.nil? or commits.first.parents.nil?
266 266 refs_map = {}
267 267 heads.each do |head|
268 268 refs_map[head.scmid] ||= []
269 269 refs_map[head.scmid] << head
270 270 end
271 271 commits_by_scmid = {}
272 272 commits.reverse.each_with_index do |commit, commit_index|
273 273 commits_by_scmid[commit.scmid] = {
274 274 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
275 275 :rdmid => commit_index,
276 276 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
277 277 :scmid => commit.scmid,
278 278 :href => block_given? ? yield(commit.scmid) : commit.scmid
279 279 }
280 280 end
281 281 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
282 282 space = nil
283 283 heads.each do |head|
284 284 if commits_by_scmid.include? head.scmid
285 285 space = index_head((space || -1) + 1, head, commits_by_scmid)
286 286 end
287 287 end
288 288 # when no head matched anything use first commit
289 289 space ||= index_head(0, commits.first, commits_by_scmid)
290 290 return commits_by_scmid, space
291 291 end
292 292
293 293 def index_head(space, commit, commits_by_scmid)
294 294 stack = [[space, commits_by_scmid[commit.scmid]]]
295 295 max_space = space
296 296 until stack.empty?
297 297 space, commit = stack.pop
298 298 commit[:space] = space if commit[:space].nil?
299 299 space -= 1
300 300 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
301 301 parent_commit = commits_by_scmid[parent_scmid]
302 302 if parent_commit and parent_commit[:space].nil?
303 303 stack.unshift [space += 1, parent_commit]
304 304 end
305 305 end
306 306 max_space = space if max_space < space
307 307 end
308 308 max_space
309 309 end
310 310 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RolesHelper
21 21 end
@@ -1,69 +1,69
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RoutesHelper
21 21
22 22 # Returns the path to project issues or to the cross-project
23 23 # issue list if project is nil
24 24 def _project_issues_path(project, *args)
25 25 if project
26 26 project_issues_path(project, *args)
27 27 else
28 28 issues_path(*args)
29 29 end
30 30 end
31 31
32 32 def _project_calendar_path(project, *args)
33 33 project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
34 34 end
35 35
36 36 def _project_gantt_path(project, *args)
37 37 project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
38 38 end
39 39
40 40 def _time_entries_path(project, issue, *args)
41 41 if issue
42 42 issue_time_entries_path(issue, *args)
43 43 elsif project
44 44 project_time_entries_path(project, *args)
45 45 else
46 46 time_entries_path(*args)
47 47 end
48 48 end
49 49
50 50 def _report_time_entries_path(project, issue, *args)
51 51 if issue
52 52 report_issue_time_entries_path(issue, *args)
53 53 elsif project
54 54 report_project_time_entries_path(project, *args)
55 55 else
56 56 report_time_entries_path(*args)
57 57 end
58 58 end
59 59
60 60 def _new_time_entry_path(project, issue, *args)
61 61 if issue
62 62 new_issue_time_entry_path(issue, *args)
63 63 elsif project
64 64 new_project_time_entry_path(project, *args)
65 65 else
66 66 new_time_entry_path(*args)
67 67 end
68 68 end
69 69 end
@@ -1,69 +1,69
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module SearchHelper
21 21 def highlight_tokens(text, tokens)
22 22 return text unless text && tokens && !tokens.empty?
23 23 re_tokens = tokens.collect {|t| Regexp.escape(t)}
24 24 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
25 25 result = ''
26 26 text.split(regexp).each_with_index do |words, i|
27 27 if result.length > 1200
28 28 # maximum length of the preview reached
29 29 result << '...'
30 30 break
31 31 end
32 32 if i.even?
33 33 result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
34 34 else
35 35 t = (tokens.index(words.downcase) || 0) % 4
36 36 result << content_tag('span', h(words), :class => "highlight token-#{t}")
37 37 end
38 38 end
39 39 result.html_safe
40 40 end
41 41
42 42 def type_label(t)
43 43 l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
44 44 end
45 45
46 46 def project_select_tag
47 47 options = [[l(:label_project_all), 'all']]
48 48 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
49 49 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
50 50 options << [@project.name, ''] unless @project.nil?
51 51 label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
52 52 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
53 53 end
54 54
55 55 def render_results_by_type(results_by_type)
56 56 links = []
57 57 # Sorts types by results count
58 58 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
59 59 c = results_by_type[t]
60 60 next if c == 0
61 61 text = "#{type_label(t)} (#{c})"
62 62 links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
63 63 :all_words => params[:all_words], :scope => params[:scope], t => 1)
64 64 end
65 65 ('<ul>'.html_safe +
66 66 links.map {|link| content_tag('li', link)}.join(' ').html_safe +
67 67 '</ul>'.html_safe) unless links.empty?
68 68 end
69 69 end
@@ -1,192 +1,192
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module SettingsHelper
21 21 def administration_settings_tabs
22 22 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
23 23 {:name => 'display', :partial => 'settings/display', :label => :label_display},
24 24 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
25 25 {:name => 'api', :partial => 'settings/api', :label => :label_api},
26 26 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
27 27 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
28 28 {:name => 'attachments', :partial => 'settings/attachments', :label => :label_attachment_plural},
29 29 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
30 30 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
31 31 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
32 32 ]
33 33 end
34 34
35 35 def setting_select(setting, choices, options={})
36 36 if blank_text = options.delete(:blank)
37 37 choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
38 38 end
39 39 setting_label(setting, options).html_safe +
40 40 select_tag("settings[#{setting}]",
41 41 options_for_select(choices, Setting.send(setting).to_s),
42 42 options).html_safe
43 43 end
44 44
45 45 def setting_multiselect(setting, choices, options={})
46 46 setting_values = Setting.send(setting)
47 47 setting_values = [] unless setting_values.is_a?(Array)
48 48
49 49 content_tag("label", l(options[:label] || "setting_#{setting}")) +
50 50 hidden_field_tag("settings[#{setting}][]", '').html_safe +
51 51 choices.collect do |choice|
52 52 text, value = (choice.is_a?(Array) ? choice : [choice, choice])
53 53 content_tag(
54 54 'label',
55 55 check_box_tag(
56 56 "settings[#{setting}][]",
57 57 value,
58 58 setting_values.include?(value),
59 59 :id => nil
60 60 ) + text.to_s,
61 61 :class => (options[:inline] ? 'inline' : 'block')
62 62 )
63 63 end.join.html_safe
64 64 end
65 65
66 66 def setting_text_field(setting, options={})
67 67 setting_label(setting, options).html_safe +
68 68 text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
69 69 end
70 70
71 71 def setting_text_area(setting, options={})
72 72 setting_label(setting, options).html_safe +
73 73 text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
74 74 end
75 75
76 76 def setting_check_box(setting, options={})
77 77 setting_label(setting, options).html_safe +
78 78 hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
79 79 check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
80 80 end
81 81
82 82 def setting_label(setting, options={})
83 83 label = options.delete(:label)
84 84 if label == false
85 85 ''
86 86 else
87 87 text = label.is_a?(String) ? label : l(label || "setting_#{setting}")
88 88 label_tag("settings_#{setting}", text, options[:label_options])
89 89 end
90 90 end
91 91
92 92 # Renders a notification field for a Redmine::Notifiable option
93 93 def notification_field(notifiable)
94 94 tag_data = notifiable.parent.present? ?
95 95 {:parent_notifiable => notifiable.parent} :
96 96 {:disables => "input[data-parent-notifiable=#{notifiable.name}]"}
97 97
98 98 tag = check_box_tag('settings[notified_events][]',
99 99 notifiable.name,
100 100 Setting.notified_events.include?(notifiable.name),
101 101 :id => nil,
102 102 :data => tag_data)
103 103
104 104 text = l_or_humanize(notifiable.name, :prefix => 'label_')
105 105
106 106 options = {}
107 107 if notifiable.parent.present?
108 108 options[:class] = "parent"
109 109 end
110 110
111 111 content_tag(:label, tag + text, options)
112 112 end
113 113
114 114 def session_lifetime_options
115 115 options = [[l(:label_disabled), 0]]
116 116 options += [4, 8, 12].map {|hours|
117 117 [l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]
118 118 }
119 119 options += [1, 7, 30, 60, 365].map {|days|
120 120 [l('datetime.distance_in_words.x_days', :count => days), (days * 24 * 60).to_s]
121 121 }
122 122 options
123 123 end
124 124
125 125 def session_timeout_options
126 126 options = [[l(:label_disabled), 0]]
127 127 options += [1, 2, 4, 8, 12, 24, 48].map {|hours|
128 128 [l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]
129 129 }
130 130 options
131 131 end
132 132
133 133 def link_copied_issue_options
134 134 options = [
135 135 [:general_text_Yes, 'yes'],
136 136 [:general_text_No, 'no'],
137 137 [:label_ask, 'ask']
138 138 ]
139 139
140 140 options.map {|label, value| [l(label), value.to_s]}
141 141 end
142 142
143 143 def cross_project_subtasks_options
144 144 options = [
145 145 [:label_disabled, ''],
146 146 [:label_cross_project_system, 'system'],
147 147 [:label_cross_project_tree, 'tree'],
148 148 [:label_cross_project_hierarchy, 'hierarchy'],
149 149 [:label_cross_project_descendants, 'descendants']
150 150 ]
151 151
152 152 options.map {|label, value| [l(label), value.to_s]}
153 153 end
154 154
155 155 def parent_issue_dates_options
156 156 options = [
157 157 [:label_parent_task_attributes_derived, 'derived'],
158 158 [:label_parent_task_attributes_independent, 'independent']
159 159 ]
160 160
161 161 options.map {|label, value| [l(label), value.to_s]}
162 162 end
163 163
164 164 def parent_issue_priority_options
165 165 options = [
166 166 [:label_parent_task_attributes_derived, 'derived'],
167 167 [:label_parent_task_attributes_independent, 'independent']
168 168 ]
169 169
170 170 options.map {|label, value| [l(label), value.to_s]}
171 171 end
172 172
173 173 def parent_issue_done_ratio_options
174 174 options = [
175 175 [:label_parent_task_attributes_derived, 'derived'],
176 176 [:label_parent_task_attributes_independent, 'independent']
177 177 ]
178 178
179 179 options.map {|label, value| [l(label), value.to_s]}
180 180 end
181 181
182 182 # Returns the options for the date_format setting
183 183 def date_format_setting_options(locale)
184 184 Setting::DATE_FORMATS.map do |f|
185 185 today = ::I18n.l(Date.today, :locale => locale, :format => f)
186 186 format = f.gsub('%', '').gsub(/[dmY]/) do
187 187 {'d' => 'dd', 'm' => 'mm', 'Y' => 'yyyy'}[$&]
188 188 end
189 189 ["#{today} (#{format})", f]
190 190 end
191 191 end
192 192 end
@@ -1,135 +1,135
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module TimelogHelper
21 21 include ApplicationHelper
22 22
23 23 def render_timelog_breadcrumb
24 24 links = []
25 25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
26 26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
27 27 if @issue
28 28 if @issue.visible?
29 29 links << link_to_issue(@issue, :subject => false)
30 30 else
31 31 links << "##{@issue.id}"
32 32 end
33 33 end
34 34 breadcrumb links
35 35 end
36 36
37 37 # Returns a collection of activities for a select field. time_entry
38 38 # is optional and will be used to check if the selected TimeEntryActivity
39 39 # is active.
40 40 def activity_collection_for_select_options(time_entry=nil, project=nil)
41 41 project ||= time_entry.try(:project)
42 42 project ||= @project
43 43 if project.nil?
44 44 activities = TimeEntryActivity.shared.active
45 45 else
46 46 activities = project.activities
47 47 end
48 48
49 49 collection = []
50 50 if time_entry && time_entry.activity && !time_entry.activity.active?
51 51 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
52 52 else
53 53 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
54 54 end
55 55 activities.each { |a| collection << [a.name, a.id] }
56 56 collection
57 57 end
58 58
59 59 def select_hours(data, criteria, value)
60 60 if value.to_s.empty?
61 61 data.select {|row| row[criteria].blank? }
62 62 else
63 63 data.select {|row| row[criteria].to_s == value.to_s}
64 64 end
65 65 end
66 66
67 67 def sum_hours(data)
68 68 sum = 0
69 69 data.each do |row|
70 70 sum += row['hours'].to_f
71 71 end
72 72 sum
73 73 end
74 74
75 75 def format_criteria_value(criteria_options, value)
76 76 if value.blank?
77 77 "[#{l(:label_none)}]"
78 78 elsif k = criteria_options[:klass]
79 79 obj = k.find_by_id(value.to_i)
80 80 if obj.is_a?(Issue)
81 81 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
82 82 else
83 83 obj
84 84 end
85 85 elsif cf = criteria_options[:custom_field]
86 86 format_value(value, cf)
87 87 else
88 88 value.to_s
89 89 end
90 90 end
91 91
92 92 def report_to_csv(report)
93 93 Redmine::Export::CSV.generate do |csv|
94 94 # Column headers
95 95 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
96 96 headers += report.periods
97 97 headers << l(:label_total_time)
98 98 csv << headers
99 99 # Content
100 100 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
101 101 # Total row
102 102 str_total = l(:label_total_time)
103 103 row = [ str_total ] + [''] * (report.criteria.size - 1)
104 104 total = 0
105 105 report.periods.each do |period|
106 106 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
107 107 total += sum
108 108 row << (sum > 0 ? sum : '')
109 109 end
110 110 row << total
111 111 csv << row
112 112 end
113 113 end
114 114
115 115 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
116 116 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
117 117 hours_for_value = select_hours(hours, criteria[level], value)
118 118 next if hours_for_value.empty?
119 119 row = [''] * level
120 120 row << format_criteria_value(available_criteria[criteria[level]], value).to_s
121 121 row += [''] * (criteria.length - level - 1)
122 122 total = 0
123 123 periods.each do |period|
124 124 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
125 125 total += sum
126 126 row << (sum > 0 ? sum : '')
127 127 end
128 128 row << total
129 129 csv << row
130 130 if criteria.length > level + 1
131 131 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
132 132 end
133 133 end
134 134 end
135 135 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module TrackersHelper
21 21 end
@@ -1,60 +1,60
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module UsersHelper
21 21 def users_status_options_for_select(selected)
22 22 user_count_by_status = User.group('status').count.to_hash
23 23 options_for_select([[l(:label_all), ''],
24 24 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
25 25 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
26 26 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
27 27 end
28 28
29 29 def user_mail_notification_options(user)
30 30 user.valid_notification_options.collect {|o| [l(o.last), o.first]}
31 31 end
32 32
33 33 def change_status_link(user)
34 34 url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
35 35
36 36 if user.locked?
37 37 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
38 38 elsif user.registered?
39 39 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
40 40 elsif user != User.current
41 41 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
42 42 end
43 43 end
44 44
45 45 def additional_emails_link(user)
46 46 if user.email_addresses.count > 1 || Setting.max_additional_emails.to_i > 0
47 47 link_to l(:label_email_address_plural), user_email_addresses_path(@user), :class => 'icon icon-email-add', :remote => true
48 48 end
49 49 end
50 50
51 51 def user_settings_tabs
52 52 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
53 53 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
54 54 ]
55 55 if Group.givable.any?
56 56 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
57 57 end
58 58 tabs
59 59 end
60 60 end
@@ -1,76 +1,76
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module VersionsHelper
21 21
22 22 def version_anchor(version)
23 23 if @project == version.project
24 24 anchor version.name
25 25 else
26 26 anchor "#{version.project.try(:identifier)}-#{version.name}"
27 27 end
28 28 end
29 29
30 30 def version_filtered_issues_path(version, options = {})
31 31 options = {:fixed_version_id => version, :set_filter => 1}.merge(options)
32 32 project = case version.sharing
33 33 when 'hierarchy', 'tree'
34 34 if version.project && version.project.root.visible?
35 35 version.project.root
36 36 else
37 37 version.project
38 38 end
39 39 when 'system'
40 40 nil
41 41 else
42 42 version.project
43 43 end
44 44
45 45 if project
46 46 project_issues_path(project, options)
47 47 else
48 48 issues_path(options)
49 49 end
50 50 end
51 51
52 52 STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
53 53
54 54 def render_issue_status_by(version, criteria)
55 55 criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
56 56
57 57 h = Hash.new {|k,v| k[v] = [0, 0]}
58 58 begin
59 59 # Total issue count
60 60 version.fixed_issues.group(criteria).count.each {|c,s| h[c][0] = s}
61 61 # Open issues count
62 62 version.fixed_issues.open.group(criteria).count.each {|c,s| h[c][1] = s}
63 63 rescue ActiveRecord::RecordNotFound
64 64 # When grouping by an association, Rails throws this exception if there's no result (bug)
65 65 end
66 66 # Sort with nil keys in last position
67 67 counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
68 68 max = counts.collect {|c| c[:total]}.max
69 69
70 70 render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
71 71 end
72 72
73 73 def status_by_options_for_select(value)
74 74 options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
75 75 end
76 76 end
@@ -1,78 +1,78
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WatchersHelper
21 21
22 22 def watcher_link(objects, user)
23 23 return '' unless user && user.logged?
24 24 objects = Array.wrap(objects)
25 25 return '' unless objects.any?
26 26
27 27 watched = Watcher.any_watched?(objects, user)
28 28 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
29 29 text = watched ? l(:button_unwatch) : l(:button_watch)
30 30 url = watch_path(
31 31 :object_type => objects.first.class.to_s.underscore,
32 32 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
33 33 )
34 34 method = watched ? 'delete' : 'post'
35 35
36 36 link_to text, url, :remote => true, :method => method, :class => css
37 37 end
38 38
39 39 # Returns the css class used to identify watch links for a given +object+
40 40 def watcher_css(objects)
41 41 objects = Array.wrap(objects)
42 42 id = (objects.size == 1 ? objects.first.id : 'bulk')
43 43 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
44 44 end
45 45
46 46 # Returns a comma separated list of users watching the given object
47 47 def watchers_list(object)
48 48 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
49 49 content = ''.html_safe
50 50 lis = object.watcher_users.collect do |user|
51 51 s = ''.html_safe
52 52 s << avatar(user, :size => "16").to_s
53 53 s << link_to_user(user, :class => 'user')
54 54 if remove_allowed
55 55 url = {:controller => 'watchers',
56 56 :action => 'destroy',
57 57 :object_type => object.class.to_s.underscore,
58 58 :object_id => object.id,
59 59 :user_id => user}
60 60 s << ' '
61 61 s << link_to(image_tag('delete.png'), url,
62 62 :remote => true, :method => 'delete', :class => "delete")
63 63 end
64 64 content << content_tag('li', s, :class => "user-#{user.id}")
65 65 end
66 66 content.present? ? content_tag('ul', content, :class => 'watchers') : content
67 67 end
68 68
69 69 def watchers_checkboxes(object, users, checked=nil)
70 70 users.map do |user|
71 71 c = checked.nil? ? object.watched_by?(user) : checked
72 72 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
73 73 content_tag 'label', "#{tag} #{h(user)}".html_safe,
74 74 :id => "issue_watcher_user_ids_#{user.id}",
75 75 :class => "floating"
76 76 end.join.html_safe
77 77 end
78 78 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WelcomeHelper
21 21 end
@@ -1,67 +1,67
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WikiHelper
21 21 include Redmine::Export::PDF::WikiPdfHelper
22 22
23 23 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
24 24 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
25 25 s = ''.html_safe
26 26 if pages.has_key?(parent)
27 27 pages[parent].each do |page|
28 28 attrs = "value='#{page.id}'"
29 29 attrs << " selected='selected'" if selected == page
30 30 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
31 31
32 32 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
33 33 wiki_page_options_for_select(pages, selected, page, level + 1)
34 34 end
35 35 end
36 36 s
37 37 end
38 38
39 39 def wiki_page_wiki_options_for_select(page)
40 40 projects = Project.allowed_to(:rename_wiki_pages).joins(:wiki).preload(:wiki).to_a
41 41 projects << page.project unless projects.include?(page.project)
42 42
43 43 project_tree_options_for_select(projects, :selected => page.project) do |project|
44 44 wiki_id = project.wiki.try(:id)
45 45 {:value => wiki_id, :selected => wiki_id == page.wiki_id}
46 46 end
47 47 end
48 48
49 49 def wiki_page_breadcrumb(page)
50 50 breadcrumb(page.ancestors.reverse.collect {|parent|
51 51 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
52 52 })
53 53 end
54 54
55 55 # Returns the path for the Cancel link when editing a wiki page
56 56 def wiki_page_edit_cancel_path(page)
57 57 if page.new_record?
58 58 if parent = page.parent
59 59 project_wiki_page_path(parent.project, parent.title)
60 60 else
61 61 project_wiki_index_path(page.project)
62 62 end
63 63 else
64 64 project_wiki_page_path(page.project, page.title)
65 65 end
66 66 end
67 67 end
@@ -1,95 +1,95
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WorkflowsHelper
21 21 def options_for_workflow_select(name, objects, selected, options={})
22 22 option_tags = ''.html_safe
23 23 multiple = false
24 24 if selected
25 25 if selected.size == objects.size
26 26 selected = 'all'
27 27 else
28 28 selected = selected.map(&:id)
29 29 if selected.size > 1
30 30 multiple = true
31 31 end
32 32 end
33 33 else
34 34 selected = objects.first.try(:id)
35 35 end
36 36 all_tag_options = {:value => 'all', :selected => (selected == 'all')}
37 37 if multiple
38 38 all_tag_options.merge!(:style => "display:none;")
39 39 end
40 40 option_tags << content_tag('option', l(:label_all), all_tag_options)
41 41 option_tags << options_from_collection_for_select(objects, "id", "name", selected)
42 42 select_tag name, option_tags, {:multiple => multiple}.merge(options)
43 43 end
44 44
45 45 def field_required?(field)
46 46 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
47 47 end
48 48
49 49 def field_permission_tag(permissions, status, field, roles)
50 50 name = field.is_a?(CustomField) ? field.id.to_s : field
51 51 options = [["", ""], [l(:label_readonly), "readonly"]]
52 52 options << [l(:label_required), "required"] unless field_required?(field)
53 53 html_options = {}
54 54
55 55 if perm = permissions[status.id][name]
56 56 if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
57 57 options << [l(:label_no_change_option), "no_change"]
58 58 selected = 'no_change'
59 59 else
60 60 selected = perm.first
61 61 end
62 62 end
63 63
64 64 hidden = field.is_a?(CustomField) &&
65 65 !field.visible? &&
66 66 !roles.detect {|role| role.custom_fields.to_a.include?(field)}
67 67
68 68 if hidden
69 69 options[0][0] = l(:label_hidden)
70 70 selected = ''
71 71 html_options[:disabled] = true
72 72 end
73 73
74 74 select_tag("permissions[#{status.id}][#{name}]", options_for_select(options, selected), html_options)
75 75 end
76 76
77 77 def transition_tag(workflows, old_status, new_status, name)
78 78 w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size
79 79
80 80 tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]"
81 81 if w == 0 || w == @roles.size * @trackers.size
82 82
83 83 hidden_field_tag(tag_name, "0", :id => nil) +
84 84 check_box_tag(tag_name, "1", w != 0,
85 85 :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}")
86 86 else
87 87 select_tag tag_name,
88 88 options_for_select([
89 89 [l(:general_text_Yes), "1"],
90 90 [l(:general_text_No), "0"],
91 91 [l(:label_no_change_option), "no_change"]
92 92 ], "no_change")
93 93 end
94 94 end
95 95 end
@@ -1,401 +1,401
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 "digest/md5"
19 19 require "fileutils"
20 20
21 21 class Attachment < ActiveRecord::Base
22 22 belongs_to :container, :polymorphic => true
23 23 belongs_to :author, :class_name => "User"
24 24
25 25 validates_presence_of :filename, :author
26 26 validates_length_of :filename, :maximum => 255
27 27 validates_length_of :disk_filename, :maximum => 255
28 28 validates_length_of :description, :maximum => 255
29 29 validate :validate_max_file_size, :validate_file_extension
30 30 attr_protected :id
31 31
32 32 acts_as_event :title => :filename,
33 33 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
34 34
35 35 acts_as_activity_provider :type => 'files',
36 36 :permission => :view_files,
37 37 :author_key => :author_id,
38 38 :scope => select("#{Attachment.table_name}.*").
39 39 joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
40 40 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )")
41 41
42 42 acts_as_activity_provider :type => 'documents',
43 43 :permission => :view_documents,
44 44 :author_key => :author_id,
45 45 :scope => select("#{Attachment.table_name}.*").
46 46 joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
47 47 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
48 48
49 49 cattr_accessor :storage_path
50 50 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
51 51
52 52 cattr_accessor :thumbnails_storage_path
53 53 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
54 54
55 55 before_create :files_to_final_location
56 56 after_commit :delete_from_disk, :on => :destroy
57 57
58 58 # Returns an unsaved copy of the attachment
59 59 def copy(attributes=nil)
60 60 copy = self.class.new
61 61 copy.attributes = self.attributes.dup.except("id", "downloads")
62 62 copy.attributes = attributes if attributes
63 63 copy
64 64 end
65 65
66 66 def validate_max_file_size
67 67 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
68 68 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
69 69 end
70 70 end
71 71
72 72 def validate_file_extension
73 73 if @temp_file
74 74 extension = File.extname(filename)
75 75 unless self.class.valid_extension?(extension)
76 76 errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
77 77 end
78 78 end
79 79 end
80 80
81 81 def file=(incoming_file)
82 82 unless incoming_file.nil?
83 83 @temp_file = incoming_file
84 84 if @temp_file.size > 0
85 85 if @temp_file.respond_to?(:original_filename)
86 86 self.filename = @temp_file.original_filename
87 87 self.filename.force_encoding("UTF-8")
88 88 end
89 89 if @temp_file.respond_to?(:content_type)
90 90 self.content_type = @temp_file.content_type.to_s.chomp
91 91 end
92 92 self.filesize = @temp_file.size
93 93 end
94 94 end
95 95 end
96 96
97 97 def file
98 98 nil
99 99 end
100 100
101 101 def filename=(arg)
102 102 write_attribute :filename, sanitize_filename(arg.to_s)
103 103 filename
104 104 end
105 105
106 106 # Copies the temporary file to its final location
107 107 # and computes its MD5 hash
108 108 def files_to_final_location
109 109 if @temp_file && (@temp_file.size > 0)
110 110 self.disk_directory = target_directory
111 111 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
112 112 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
113 113 path = File.dirname(diskfile)
114 114 unless File.directory?(path)
115 115 FileUtils.mkdir_p(path)
116 116 end
117 117 md5 = Digest::MD5.new
118 118 File.open(diskfile, "wb") do |f|
119 119 if @temp_file.respond_to?(:read)
120 120 buffer = ""
121 121 while (buffer = @temp_file.read(8192))
122 122 f.write(buffer)
123 123 md5.update(buffer)
124 124 end
125 125 else
126 126 f.write(@temp_file)
127 127 md5.update(@temp_file)
128 128 end
129 129 end
130 130 self.digest = md5.hexdigest
131 131 end
132 132 @temp_file = nil
133 133
134 134 if content_type.blank? && filename.present?
135 135 self.content_type = Redmine::MimeType.of(filename)
136 136 end
137 137 # Don't save the content type if it's longer than the authorized length
138 138 if self.content_type && self.content_type.length > 255
139 139 self.content_type = nil
140 140 end
141 141 end
142 142
143 143 # Deletes the file from the file system if it's not referenced by other attachments
144 144 def delete_from_disk
145 145 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
146 146 delete_from_disk!
147 147 end
148 148 end
149 149
150 150 # Returns file's location on disk
151 151 def diskfile
152 152 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
153 153 end
154 154
155 155 def title
156 156 title = filename.to_s
157 157 if description.present?
158 158 title << " (#{description})"
159 159 end
160 160 title
161 161 end
162 162
163 163 def increment_download
164 164 increment!(:downloads)
165 165 end
166 166
167 167 def project
168 168 container.try(:project)
169 169 end
170 170
171 171 def visible?(user=User.current)
172 172 if container_id
173 173 container && container.attachments_visible?(user)
174 174 else
175 175 author == user
176 176 end
177 177 end
178 178
179 179 def editable?(user=User.current)
180 180 if container_id
181 181 container && container.attachments_editable?(user)
182 182 else
183 183 author == user
184 184 end
185 185 end
186 186
187 187 def deletable?(user=User.current)
188 188 if container_id
189 189 container && container.attachments_deletable?(user)
190 190 else
191 191 author == user
192 192 end
193 193 end
194 194
195 195 def image?
196 196 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
197 197 end
198 198
199 199 def thumbnailable?
200 200 image?
201 201 end
202 202
203 203 # Returns the full path the attachment thumbnail, or nil
204 204 # if the thumbnail cannot be generated.
205 205 def thumbnail(options={})
206 206 if thumbnailable? && readable?
207 207 size = options[:size].to_i
208 208 if size > 0
209 209 # Limit the number of thumbnails per image
210 210 size = (size / 50) * 50
211 211 # Maximum thumbnail size
212 212 size = 800 if size > 800
213 213 else
214 214 size = Setting.thumbnails_size.to_i
215 215 end
216 216 size = 100 unless size > 0
217 217 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
218 218
219 219 begin
220 220 Redmine::Thumbnail.generate(self.diskfile, target, size)
221 221 rescue => e
222 222 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
223 223 return nil
224 224 end
225 225 end
226 226 end
227 227
228 228 # Deletes all thumbnails
229 229 def self.clear_thumbnails
230 230 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
231 231 File.delete file
232 232 end
233 233 end
234 234
235 235 def is_text?
236 236 Redmine::MimeType.is_type?('text', filename)
237 237 end
238 238
239 239 def is_diff?
240 240 self.filename =~ /\.(patch|diff)$/i
241 241 end
242 242
243 243 # Returns true if the file is readable
244 244 def readable?
245 245 File.readable?(diskfile)
246 246 end
247 247
248 248 # Returns the attachment token
249 249 def token
250 250 "#{id}.#{digest}"
251 251 end
252 252
253 253 # Finds an attachment that matches the given token and that has no container
254 254 def self.find_by_token(token)
255 255 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
256 256 attachment_id, attachment_digest = $1, $2
257 257 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
258 258 if attachment && attachment.container.nil?
259 259 attachment
260 260 end
261 261 end
262 262 end
263 263
264 264 # Bulk attaches a set of files to an object
265 265 #
266 266 # Returns a Hash of the results:
267 267 # :files => array of the attached files
268 268 # :unsaved => array of the files that could not be attached
269 269 def self.attach_files(obj, attachments)
270 270 result = obj.save_attachments(attachments, User.current)
271 271 obj.attach_saved_attachments
272 272 result
273 273 end
274 274
275 275 # Updates the filename and description of a set of attachments
276 276 # with the given hash of attributes. Returns true if all
277 277 # attachments were updated.
278 278 #
279 279 # Example:
280 280 # Attachment.update_attachments(attachments, {
281 281 # 4 => {:filename => 'foo'},
282 282 # 7 => {:filename => 'bar', :description => 'file description'}
283 283 # })
284 284 #
285 285 def self.update_attachments(attachments, params)
286 286 params = params.transform_keys {|key| key.to_i}
287 287
288 288 saved = true
289 289 transaction do
290 290 attachments.each do |attachment|
291 291 if p = params[attachment.id]
292 292 attachment.filename = p[:filename] if p.key?(:filename)
293 293 attachment.description = p[:description] if p.key?(:description)
294 294 saved &&= attachment.save
295 295 end
296 296 end
297 297 unless saved
298 298 raise ActiveRecord::Rollback
299 299 end
300 300 end
301 301 saved
302 302 end
303 303
304 304 def self.latest_attach(attachments, filename)
305 305 attachments.sort_by(&:created_on).reverse.detect do |att|
306 306 filename.casecmp(att.filename) == 0
307 307 end
308 308 end
309 309
310 310 def self.prune(age=1.day)
311 311 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
312 312 end
313 313
314 314 # Moves an existing attachment to its target directory
315 315 def move_to_target_directory!
316 316 return unless !new_record? & readable?
317 317
318 318 src = diskfile
319 319 self.disk_directory = target_directory
320 320 dest = diskfile
321 321
322 322 return if src == dest
323 323
324 324 if !FileUtils.mkdir_p(File.dirname(dest))
325 325 logger.error "Could not create directory #{File.dirname(dest)}" if logger
326 326 return
327 327 end
328 328
329 329 if !FileUtils.mv(src, dest)
330 330 logger.error "Could not move attachment from #{src} to #{dest}" if logger
331 331 return
332 332 end
333 333
334 334 update_column :disk_directory, disk_directory
335 335 end
336 336
337 337 # Moves existing attachments that are stored at the root of the files
338 338 # directory (ie. created before Redmine 2.3) to their target subdirectories
339 339 def self.move_from_root_to_target_directory
340 340 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
341 341 attachment.move_to_target_directory!
342 342 end
343 343 end
344 344
345 345 # Returns true if the extension is allowed, otherwise false
346 346 def self.valid_extension?(extension)
347 347 extension = extension.downcase.sub(/\A\.+/, '')
348 348
349 349 denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting|
350 350 Setting.send(setting).to_s.split(",").map {|s| s.strip.downcase.sub(/\A\.+/, '')}.reject(&:blank?)
351 351 end
352 352 if denied.present? && denied.include?(extension)
353 353 return false
354 354 end
355 355 unless allowed.blank? || allowed.include?(extension)
356 356 return false
357 357 end
358 358 true
359 359 end
360 360
361 361 private
362 362
363 363 # Physically deletes the file from the file system
364 364 def delete_from_disk!
365 365 if disk_filename.present? && File.exist?(diskfile)
366 366 File.delete(diskfile)
367 367 end
368 368 end
369 369
370 370 def sanitize_filename(value)
371 371 # get only the filename, not the whole path
372 372 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
373 373
374 374 # Finally, replace invalid characters with underscore
375 375 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
376 376 end
377 377
378 378 # Returns the subdirectory in which the attachment will be saved
379 379 def target_directory
380 380 time = created_on || DateTime.now
381 381 time.strftime("%Y/%m")
382 382 end
383 383
384 384 # Returns an ASCII or hashed filename that do not
385 385 # exists yet in the given subdirectory
386 386 def self.disk_filename(filename, directory=nil)
387 387 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
388 388 ascii = ''
389 389 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
390 390 ascii = filename
391 391 else
392 392 ascii = Digest::MD5.hexdigest(filename)
393 393 # keep the extension if any
394 394 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
395 395 end
396 396 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
397 397 timestamp.succ!
398 398 end
399 399 "#{timestamp}_#{ascii}"
400 400 end
401 401 end
@@ -1,93 +1,93
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 # Generic exception for when the AuthSource can not be reached
19 19 # (eg. can not connect to the LDAP)
20 20 class AuthSourceException < Exception; end
21 21 class AuthSourceTimeoutException < AuthSourceException; end
22 22
23 23 class AuthSource < ActiveRecord::Base
24 24 include Redmine::SubclassFactory
25 25 include Redmine::Ciphering
26 26
27 27 has_many :users
28 28
29 29 validates_presence_of :name
30 30 validates_uniqueness_of :name
31 31 validates_length_of :name, :maximum => 60
32 32 attr_protected :id
33 33
34 34 def authenticate(login, password)
35 35 end
36 36
37 37 def test_connection
38 38 end
39 39
40 40 def auth_method_name
41 41 "Abstract"
42 42 end
43 43
44 44 def account_password
45 45 read_ciphered_attribute(:account_password)
46 46 end
47 47
48 48 def account_password=(arg)
49 49 write_ciphered_attribute(:account_password, arg)
50 50 end
51 51
52 52 def searchable?
53 53 false
54 54 end
55 55
56 56 def self.search(q)
57 57 results = []
58 58 AuthSource.all.each do |source|
59 59 begin
60 60 if source.searchable?
61 61 results += source.search(q)
62 62 end
63 63 rescue AuthSourceException => e
64 64 logger.error "Error while searching users in #{source.name}: #{e.message}"
65 65 end
66 66 end
67 67 results
68 68 end
69 69
70 70 def allow_password_changes?
71 71 self.class.allow_password_changes?
72 72 end
73 73
74 74 # Does this auth source backend allow password changes?
75 75 def self.allow_password_changes?
76 76 false
77 77 end
78 78
79 79 # Try to authenticate a user not yet registered against available sources
80 80 def self.authenticate(login, password)
81 81 AuthSource.where(:onthefly_register => true).each do |source|
82 82 begin
83 83 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
84 84 attrs = source.authenticate(login, password)
85 85 rescue => e
86 86 logger.error "Error during authentication: #{e.message}"
87 87 attrs = nil
88 88 end
89 89 return attrs if attrs
90 90 end
91 91 return nil
92 92 end
93 93 end
@@ -1,204 +1,204
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 'net/ldap'
19 19 require 'net/ldap/dn'
20 20 require 'timeout'
21 21
22 22 class AuthSourceLdap < AuthSource
23 23 NETWORK_EXCEPTIONS = [
24 24 Net::LDAP::LdapError,
25 25 Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET,
26 26 Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
27 27 SocketError
28 28 ]
29 29
30 30 validates_presence_of :host, :port, :attr_login
31 31 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
32 32 validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_blank => true
33 33 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
34 34 validates_numericality_of :port, :only_integer => true
35 35 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
36 36 validate :validate_filter
37 37
38 38 before_validation :strip_ldap_attributes
39 39
40 40 def initialize(attributes=nil, *args)
41 41 super
42 42 self.port = 389 if self.port == 0
43 43 end
44 44
45 45 def authenticate(login, password)
46 46 return nil if login.blank? || password.blank?
47 47
48 48 with_timeout do
49 49 attrs = get_user_dn(login, password)
50 50 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
51 51 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
52 52 return attrs.except(:dn)
53 53 end
54 54 end
55 55 rescue *NETWORK_EXCEPTIONS => e
56 56 raise AuthSourceException.new(e.message)
57 57 end
58 58
59 59 # test the connection to the LDAP
60 60 def test_connection
61 61 with_timeout do
62 62 ldap_con = initialize_ldap_con(self.account, self.account_password)
63 63 ldap_con.open { }
64 64 end
65 65 rescue *NETWORK_EXCEPTIONS => e
66 66 raise AuthSourceException.new(e.message)
67 67 end
68 68
69 69 def auth_method_name
70 70 "LDAP"
71 71 end
72 72
73 73 # Returns true if this source can be searched for users
74 74 def searchable?
75 75 !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
76 76 end
77 77
78 78 # Searches the source for users and returns an array of results
79 79 def search(q)
80 80 q = q.to_s.strip
81 81 return [] unless searchable? && q.present?
82 82
83 83 results = []
84 84 search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
85 85 ldap_con = initialize_ldap_con(self.account, self.account_password)
86 86 ldap_con.search(:base => self.base_dn,
87 87 :filter => search_filter,
88 88 :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
89 89 :size => 10) do |entry|
90 90 attrs = get_user_attributes_from_ldap_entry(entry)
91 91 attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
92 92 results << attrs
93 93 end
94 94 results
95 95 rescue *NETWORK_EXCEPTIONS => e
96 96 raise AuthSourceException.new(e.message)
97 97 end
98 98
99 99 private
100 100
101 101 def with_timeout(&block)
102 102 timeout = self.timeout
103 103 timeout = 20 unless timeout && timeout > 0
104 104 Timeout.timeout(timeout) do
105 105 return yield
106 106 end
107 107 rescue Timeout::Error => e
108 108 raise AuthSourceTimeoutException.new(e.message)
109 109 end
110 110
111 111 def ldap_filter
112 112 if filter.present?
113 113 Net::LDAP::Filter.construct(filter)
114 114 end
115 115 rescue Net::LDAP::LdapError, Net::LDAP::FilterSyntaxInvalidError
116 116 nil
117 117 end
118 118
119 119 def base_filter
120 120 filter = Net::LDAP::Filter.eq("objectClass", "*")
121 121 if f = ldap_filter
122 122 filter = filter & f
123 123 end
124 124 filter
125 125 end
126 126
127 127 def validate_filter
128 128 if filter.present? && ldap_filter.nil?
129 129 errors.add(:filter, :invalid)
130 130 end
131 131 end
132 132
133 133 def strip_ldap_attributes
134 134 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
135 135 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
136 136 end
137 137 end
138 138
139 139 def initialize_ldap_con(ldap_user, ldap_password)
140 140 options = { :host => self.host,
141 141 :port => self.port,
142 142 :encryption => (self.tls ? :simple_tls : nil)
143 143 }
144 144 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
145 145 Net::LDAP.new options
146 146 end
147 147
148 148 def get_user_attributes_from_ldap_entry(entry)
149 149 {
150 150 :dn => entry.dn,
151 151 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
152 152 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
153 153 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
154 154 :auth_source_id => self.id
155 155 }
156 156 end
157 157
158 158 # Return the attributes needed for the LDAP search. It will only
159 159 # include the user attributes if on-the-fly registration is enabled
160 160 def search_attributes
161 161 if onthefly_register?
162 162 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
163 163 else
164 164 ['dn']
165 165 end
166 166 end
167 167
168 168 # Check if a DN (user record) authenticates with the password
169 169 def authenticate_dn(dn, password)
170 170 if dn.present? && password.present?
171 171 initialize_ldap_con(dn, password).bind
172 172 end
173 173 end
174 174
175 175 # Get the user's dn and any attributes for them, given their login
176 176 def get_user_dn(login, password)
177 177 ldap_con = nil
178 178 if self.account && self.account.include?("$login")
179 179 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
180 180 else
181 181 ldap_con = initialize_ldap_con(self.account, self.account_password)
182 182 end
183 183 attrs = {}
184 184 search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
185 185 ldap_con.search( :base => self.base_dn,
186 186 :filter => search_filter,
187 187 :attributes=> search_attributes) do |entry|
188 188 if onthefly_register?
189 189 attrs = get_user_attributes_from_ldap_entry(entry)
190 190 else
191 191 attrs = {:dn => entry.dn}
192 192 end
193 193 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
194 194 end
195 195 attrs
196 196 end
197 197
198 198 def self.get_attr(entry, attr_name)
199 199 if !attr_name.blank?
200 200 value = entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
201 201 value.to_s.force_encoding('UTF-8')
202 202 end
203 203 end
204 204 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Board < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 has_many :messages, lambda {order("#{Message.table_name}.created_on DESC")}, :dependent => :destroy
22 22 belongs_to :last_message, :class_name => 'Message'
23 23 acts_as_tree :dependent => :nullify
24 24 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
25 25 acts_as_watchable
26 26
27 27 validates_presence_of :name, :description
28 28 validates_length_of :name, :maximum => 30
29 29 validates_length_of :description, :maximum => 255
30 30 validate :validate_board
31 31 attr_protected :id
32 32
33 33 scope :visible, lambda {|*args|
34 34 joins(:project).
35 35 where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
36 36 }
37 37
38 38 safe_attributes 'name', 'description', 'parent_id', 'move_to'
39 39
40 40 def visible?(user=User.current)
41 41 !user.nil? && user.allowed_to?(:view_messages, project)
42 42 end
43 43
44 44 def reload(*args)
45 45 @valid_parents = nil
46 46 super
47 47 end
48 48
49 49 def to_s
50 50 name
51 51 end
52 52
53 53 # Returns a scope for the board topics (messages without parent)
54 54 def topics
55 55 messages.where(:parent_id => nil)
56 56 end
57 57
58 58 def valid_parents
59 59 @valid_parents ||= project.boards - self_and_descendants
60 60 end
61 61
62 62 def reset_counters!
63 63 self.class.reset_counters!(id)
64 64 end
65 65
66 66 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
67 67 def self.reset_counters!(board_id)
68 68 board_id = board_id.to_i
69 69 Board.where(:id => board_id).
70 70 update_all(["topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=:id AND parent_id IS NULL)," +
71 71 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=:id)," +
72 72 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=:id)", :id => board_id])
73 73 end
74 74
75 75 def self.board_tree(boards, parent_id=nil, level=0)
76 76 tree = []
77 77 boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
78 78 tree << [board, level]
79 79 tree += board_tree(boards, board.id, level+1)
80 80 end
81 81 if block_given?
82 82 tree.each do |board, level|
83 83 yield board, level
84 84 end
85 85 end
86 86 tree
87 87 end
88 88
89 89 protected
90 90
91 91 def validate_board
92 92 if parent_id && parent_id_changed?
93 93 errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
94 94 end
95 95 end
96 96 end
@@ -1,34 +1,34
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Change < ActiveRecord::Base
19 19 belongs_to :changeset
20 20
21 21 validates_presence_of :changeset_id, :action, :path
22 22 before_save :init_path
23 23 before_validation :replace_invalid_utf8_of_path
24 24 attr_protected :id
25 25
26 26 def replace_invalid_utf8_of_path
27 27 self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
28 28 self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
29 29 end
30 30
31 31 def init_path
32 32 self.path ||= ""
33 33 end
34 34 end
@@ -1,294 +1,294
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 belongs_to :user
21 21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
22 22 has_and_belongs_to_many :issues
23 23 has_and_belongs_to_many :parents,
24 24 :class_name => "Changeset",
25 25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
26 26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
27 27 has_and_belongs_to_many :children,
28 28 :class_name => "Changeset",
29 29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
30 30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
31 31
32 32 acts_as_event :title => Proc.new {|o| o.title},
33 33 :description => :long_comments,
34 34 :datetime => :committed_on,
35 35 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
36 36
37 37 acts_as_searchable :columns => 'comments',
38 38 :preload => {:repository => :project},
39 39 :project_key => "#{Repository.table_name}.project_id",
40 40 :date_column => :committed_on
41 41
42 42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
43 43 :author_key => :user_id,
44 44 :scope => preload(:user, {:repository => :project})
45 45
46 46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
47 47 validates_uniqueness_of :revision, :scope => :repository_id
48 48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
49 49 attr_protected :id
50 50
51 51 scope :visible, lambda {|*args|
52 52 joins(:repository => :project).
53 53 where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
54 54 }
55 55
56 56 after_create :scan_for_issues
57 57 before_create :before_create_cs
58 58
59 59 def revision=(r)
60 60 write_attribute :revision, (r.nil? ? nil : r.to_s)
61 61 end
62 62
63 63 # Returns the identifier of this changeset; depending on repository backends
64 64 def identifier
65 65 if repository.class.respond_to? :changeset_identifier
66 66 repository.class.changeset_identifier self
67 67 else
68 68 revision.to_s
69 69 end
70 70 end
71 71
72 72 def committed_on=(date)
73 73 self.commit_date = date
74 74 super
75 75 end
76 76
77 77 # Returns the readable identifier
78 78 def format_identifier
79 79 if repository.class.respond_to? :format_changeset_identifier
80 80 repository.class.format_changeset_identifier self
81 81 else
82 82 identifier
83 83 end
84 84 end
85 85
86 86 def project
87 87 repository.project
88 88 end
89 89
90 90 def author
91 91 user || committer.to_s.split('<').first
92 92 end
93 93
94 94 def before_create_cs
95 95 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
96 96 self.comments = self.class.normalize_comments(
97 97 self.comments, repository.repo_log_encoding)
98 98 self.user = repository.find_committer_user(self.committer)
99 99 end
100 100
101 101 def scan_for_issues
102 102 scan_comment_for_issue_ids
103 103 end
104 104
105 105 TIMELOG_RE = /
106 106 (
107 107 ((\d+)(h|hours?))((\d+)(m|min)?)?
108 108 |
109 109 ((\d+)(h|hours?|m|min))
110 110 |
111 111 (\d+):(\d+)
112 112 |
113 113 (\d+([\.,]\d+)?)h?
114 114 )
115 115 /x
116 116
117 117 def scan_comment_for_issue_ids
118 118 return if comments.blank?
119 119 # keywords used to reference issues
120 120 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
121 121 ref_keywords_any = ref_keywords.delete('*')
122 122 # keywords used to fix issues
123 123 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
124 124
125 125 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
126 126
127 127 referenced_issues = []
128 128
129 129 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
130 130 action, refs = match[2].to_s.downcase, match[3]
131 131 next unless action.present? || ref_keywords_any
132 132
133 133 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
134 134 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
135 135 if issue && !issue_linked_to_same_commit?(issue)
136 136 referenced_issues << issue
137 137 # Don't update issues or log time when importing old commits
138 138 unless repository.created_on && committed_on && committed_on < repository.created_on
139 139 fix_issue(issue, action) if fix_keywords.include?(action)
140 140 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
141 141 end
142 142 end
143 143 end
144 144 end
145 145
146 146 referenced_issues.uniq!
147 147 self.issues = referenced_issues unless referenced_issues.empty?
148 148 end
149 149
150 150 def short_comments
151 151 @short_comments || split_comments.first
152 152 end
153 153
154 154 def long_comments
155 155 @long_comments || split_comments.last
156 156 end
157 157
158 158 def text_tag(ref_project=nil)
159 159 repo = ""
160 160 if repository && repository.identifier.present?
161 161 repo = "#{repository.identifier}|"
162 162 end
163 163 tag = if scmid?
164 164 "commit:#{repo}#{scmid}"
165 165 else
166 166 "#{repo}r#{revision}"
167 167 end
168 168 if ref_project && project && ref_project != project
169 169 tag = "#{project.identifier}:#{tag}"
170 170 end
171 171 tag
172 172 end
173 173
174 174 # Returns the title used for the changeset in the activity/search results
175 175 def title
176 176 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
177 177 comm = short_comments.blank? ? '' : (': ' + short_comments)
178 178 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
179 179 end
180 180
181 181 # Returns the previous changeset
182 182 def previous
183 183 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
184 184 end
185 185
186 186 # Returns the next changeset
187 187 def next
188 188 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
189 189 end
190 190
191 191 # Creates a new Change from it's common parameters
192 192 def create_change(change)
193 193 Change.create(:changeset => self,
194 194 :action => change[:action],
195 195 :path => change[:path],
196 196 :from_path => change[:from_path],
197 197 :from_revision => change[:from_revision])
198 198 end
199 199
200 200 # Finds an issue that can be referenced by the commit message
201 201 def find_referenced_issue_by_id(id)
202 202 return nil if id.blank?
203 203 issue = Issue.find_by_id(id.to_i)
204 204 if Setting.commit_cross_project_ref?
205 205 # all issues can be referenced/fixed
206 206 elsif issue
207 207 # issue that belong to the repository project, a subproject or a parent project only
208 208 unless issue.project &&
209 209 (project == issue.project || project.is_ancestor_of?(issue.project) ||
210 210 project.is_descendant_of?(issue.project))
211 211 issue = nil
212 212 end
213 213 end
214 214 issue
215 215 end
216 216
217 217 private
218 218
219 219 # Returns true if the issue is already linked to the same commit
220 220 # from a different repository
221 221 def issue_linked_to_same_commit?(issue)
222 222 repository.same_commits_in_scope(issue.changesets, self).any?
223 223 end
224 224
225 225 # Updates the +issue+ according to +action+
226 226 def fix_issue(issue, action)
227 227 # the issue may have been updated by the closure of another one (eg. duplicate)
228 228 issue.reload
229 229 # don't change the status is the issue is closed
230 230 return if issue.closed?
231 231
232 232 journal = issue.init_journal(user || User.anonymous,
233 233 ll(Setting.default_language,
234 234 :text_status_changed_by_changeset,
235 235 text_tag(issue.project)))
236 236 rule = Setting.commit_update_keywords_array.detect do |rule|
237 237 rule['keywords'].include?(action) &&
238 238 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
239 239 end
240 240 if rule
241 241 issue.assign_attributes rule.slice(*Issue.attribute_names)
242 242 end
243 243 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
244 244 { :changeset => self, :issue => issue, :action => action })
245 245
246 246 if issue.changes.any?
247 247 unless issue.save
248 248 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
249 249 end
250 250 end
251 251 issue
252 252 end
253 253
254 254 def log_time(issue, hours)
255 255 time_entry = TimeEntry.new(
256 256 :user => user,
257 257 :hours => hours,
258 258 :issue => issue,
259 259 :spent_on => commit_date,
260 260 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
261 261 :locale => Setting.default_language)
262 262 )
263 263 time_entry.activity = log_time_activity unless log_time_activity.nil?
264 264
265 265 unless time_entry.save
266 266 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
267 267 end
268 268 time_entry
269 269 end
270 270
271 271 def log_time_activity
272 272 if Setting.commit_logtime_activity_id.to_i > 0
273 273 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
274 274 end
275 275 end
276 276
277 277 def split_comments
278 278 comments =~ /\A(.+?)\r?\n(.*)$/m
279 279 @short_comments = $1 || comments
280 280 @long_comments = $2.to_s.strip
281 281 return @short_comments, @long_comments
282 282 end
283 283
284 284 public
285 285
286 286 # Strips and reencodes a commit log before insertion into the database
287 287 def self.normalize_comments(str, encoding)
288 288 Changeset.to_utf8(str.to_s.strip, encoding)
289 289 end
290 290
291 291 def self.to_utf8(str, encoding)
292 292 Redmine::CodesetUtil.to_utf8(str, encoding)
293 293 end
294 294 end
@@ -1,38 +1,38
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Comment < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :commented, :polymorphic => true, :counter_cache => true
21 21 belongs_to :author, :class_name => 'User'
22 22
23 23 validates_presence_of :commented, :author, :comments
24 24 attr_protected :id
25 25
26 26 after_create :send_notification
27 27
28 28 safe_attributes 'comments'
29 29
30 30 private
31 31
32 32 def send_notification
33 33 mailer_method = "#{commented.class.name.underscore}_comment_added"
34 34 if Setting.notified_events.include?(mailer_method)
35 35 Mailer.send(mailer_method, self).deliver
36 36 end
37 37 end
38 38 end
@@ -1,284 +1,284
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CustomField < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 has_many :enumerations,
22 22 lambda { order(:position) },
23 23 :class_name => 'CustomFieldEnumeration',
24 24 :dependent => :delete_all
25 25 has_many :custom_values, :dependent => :delete_all
26 26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 27 acts_as_list :scope => 'type = \'#{self.class}\''
28 28 serialize :possible_values
29 29 store :format_store
30 30
31 31 validates_presence_of :name, :field_format
32 32 validates_uniqueness_of :name, :scope => :type
33 33 validates_length_of :name, :maximum => 30
34 34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
35 35 validate :validate_custom_field
36 36 attr_protected :id
37 37
38 38 before_validation :set_searchable
39 39 before_save do |field|
40 40 field.format.before_custom_field_save(field)
41 41 end
42 42 after_save :handle_multiplicity_change
43 43 after_save do |field|
44 44 if field.visible_changed? && field.visible
45 45 field.roles.clear
46 46 end
47 47 end
48 48
49 49 scope :sorted, lambda { order(:position) }
50 50 scope :visible, lambda {|*args|
51 51 user = args.shift || User.current
52 52 if user.admin?
53 53 # nop
54 54 elsif user.memberships.any?
55 55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
56 56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
57 57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
58 58 " WHERE m.user_id = ?)",
59 59 true, user.id)
60 60 else
61 61 where(:visible => true)
62 62 end
63 63 }
64 64
65 65 def visible_by?(project, user=User.current)
66 66 visible? || user.admin?
67 67 end
68 68
69 69 def format
70 70 @format ||= Redmine::FieldFormat.find(field_format)
71 71 end
72 72
73 73 def field_format=(arg)
74 74 # cannot change format of a saved custom field
75 75 if new_record?
76 76 @format = nil
77 77 super
78 78 end
79 79 end
80 80
81 81 def set_searchable
82 82 # make sure these fields are not searchable
83 83 self.searchable = false unless format.class.searchable_supported
84 84 # make sure only these fields can have multiple values
85 85 self.multiple = false unless format.class.multiple_supported
86 86 true
87 87 end
88 88
89 89 def validate_custom_field
90 90 format.validate_custom_field(self).each do |attribute, message|
91 91 errors.add attribute, message
92 92 end
93 93
94 94 if regexp.present?
95 95 begin
96 96 Regexp.new(regexp)
97 97 rescue
98 98 errors.add(:regexp, :invalid)
99 99 end
100 100 end
101 101
102 102 if default_value.present?
103 103 validate_field_value(default_value).each do |message|
104 104 errors.add :default_value, message
105 105 end
106 106 end
107 107 end
108 108
109 109 def possible_custom_value_options(custom_value)
110 110 format.possible_custom_value_options(custom_value)
111 111 end
112 112
113 113 def possible_values_options(object=nil)
114 114 if object.is_a?(Array)
115 115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
116 116 else
117 117 format.possible_values_options(self, object) || []
118 118 end
119 119 end
120 120
121 121 def possible_values
122 122 values = read_attribute(:possible_values)
123 123 if values.is_a?(Array)
124 124 values.each do |value|
125 125 value.to_s.force_encoding('UTF-8')
126 126 end
127 127 values
128 128 else
129 129 []
130 130 end
131 131 end
132 132
133 133 # Makes possible_values accept a multiline string
134 134 def possible_values=(arg)
135 135 if arg.is_a?(Array)
136 136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
137 137 write_attribute(:possible_values, values)
138 138 else
139 139 self.possible_values = arg.to_s.split(/[\n\r]+/)
140 140 end
141 141 end
142 142
143 143 def cast_value(value)
144 144 format.cast_value(self, value)
145 145 end
146 146
147 147 def value_from_keyword(keyword, customized)
148 148 format.value_from_keyword(self, keyword, customized)
149 149 end
150 150
151 151 # Returns the options hash used to build a query filter for the field
152 152 def query_filter_options(query)
153 153 format.query_filter_options(self, query)
154 154 end
155 155
156 156 def totalable?
157 157 format.totalable_supported
158 158 end
159 159
160 160 # Returns a ORDER BY clause that can used to sort customized
161 161 # objects by their value of the custom field.
162 162 # Returns nil if the custom field can not be used for sorting.
163 163 def order_statement
164 164 return nil if multiple?
165 165 format.order_statement(self)
166 166 end
167 167
168 168 # Returns a GROUP BY clause that can used to group by custom value
169 169 # Returns nil if the custom field can not be used for grouping.
170 170 def group_statement
171 171 return nil if multiple?
172 172 format.group_statement(self)
173 173 end
174 174
175 175 def join_for_order_statement
176 176 format.join_for_order_statement(self)
177 177 end
178 178
179 179 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
180 180 if visible? || user.admin?
181 181 "1=1"
182 182 elsif user.anonymous?
183 183 "1=0"
184 184 else
185 185 project_key ||= "#{self.class.customized_class.table_name}.project_id"
186 186 id_column ||= id
187 187 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
188 188 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
189 189 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
190 190 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
191 191 end
192 192 end
193 193
194 194 def self.visibility_condition
195 195 if user.admin?
196 196 "1=1"
197 197 elsif user.anonymous?
198 198 "#{table_name}.visible"
199 199 else
200 200 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
201 201 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
202 202 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
203 203 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
204 204 end
205 205 end
206 206
207 207 def <=>(field)
208 208 position <=> field.position
209 209 end
210 210
211 211 # Returns the class that values represent
212 212 def value_class
213 213 format.target_class if format.respond_to?(:target_class)
214 214 end
215 215
216 216 def self.customized_class
217 217 self.name =~ /^(.+)CustomField$/
218 218 $1.constantize rescue nil
219 219 end
220 220
221 221 # to move in project_custom_field
222 222 def self.for_all
223 223 where(:is_for_all => true).order('position').to_a
224 224 end
225 225
226 226 def type_name
227 227 nil
228 228 end
229 229
230 230 # Returns the error messages for the given value
231 231 # or an empty array if value is a valid value for the custom field
232 232 def validate_custom_value(custom_value)
233 233 value = custom_value.value
234 234 errs = []
235 235 if value.is_a?(Array)
236 236 if !multiple?
237 237 errs << ::I18n.t('activerecord.errors.messages.invalid')
238 238 end
239 239 if is_required? && value.detect(&:present?).nil?
240 240 errs << ::I18n.t('activerecord.errors.messages.blank')
241 241 end
242 242 else
243 243 if is_required? && value.blank?
244 244 errs << ::I18n.t('activerecord.errors.messages.blank')
245 245 end
246 246 end
247 247 errs += format.validate_custom_value(custom_value)
248 248 errs
249 249 end
250 250
251 251 # Returns the error messages for the default custom field value
252 252 def validate_field_value(value)
253 253 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
254 254 end
255 255
256 256 # Returns true if value is a valid value for the custom field
257 257 def valid_field_value?(value)
258 258 validate_field_value(value).empty?
259 259 end
260 260
261 261 def format_in?(*args)
262 262 args.include?(field_format)
263 263 end
264 264
265 265 protected
266 266
267 267 # Removes multiple values for the custom field after setting the multiple attribute to false
268 268 # We kepp the value with the highest id for each customized object
269 269 def handle_multiplicity_change
270 270 if !new_record? && multiple_was && !multiple
271 271 ids = custom_values.
272 272 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
273 273 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
274 274 " AND cve.id > #{CustomValue.table_name}.id)").
275 275 pluck(:id)
276 276
277 277 if ids.any?
278 278 custom_values.where(:id => ids).delete_all
279 279 end
280 280 end
281 281 end
282 282 end
283 283
284 284 require_dependency 'redmine/field_format'
@@ -1,80 +1,80
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CustomFieldEnumeration < ActiveRecord::Base
19 19 belongs_to :custom_field
20 20 attr_accessible :name, :active, :position
21 21
22 22 validates_presence_of :name, :position, :custom_field_id
23 23 validates_length_of :name, :maximum => 60
24 24 validates_numericality_of :position, :only_integer => true
25 25 before_create :set_position
26 26
27 27 scope :active, lambda { where(:active => true) }
28 28
29 29 def to_s
30 30 name.to_s
31 31 end
32 32
33 33 def objects_count
34 34 custom_values.count
35 35 end
36 36
37 37 def in_use?
38 38 objects_count > 0
39 39 end
40 40
41 41 alias :destroy_without_reassign :destroy
42 42 def destroy(reassign_to=nil)
43 43 if reassign_to
44 44 custom_values.update_all(:value => reassign_to.id.to_s)
45 45 end
46 46 destroy_without_reassign
47 47 end
48 48
49 49 def custom_values
50 50 custom_field.custom_values.where(:value => id.to_s)
51 51 end
52 52
53 53 def self.update_each(custom_field, attributes)
54 54 return unless attributes.is_a?(Hash)
55 55 transaction do
56 56 attributes.each do |enumeration_id, enumeration_attributes|
57 57 enumeration = custom_field.enumerations.find_by_id(enumeration_id)
58 58 if enumeration
59 59 enumeration.attributes = enumeration_attributes
60 60 unless enumeration.save
61 61 raise ActiveRecord::Rollback
62 62 end
63 63 end
64 64 end
65 65 end
66 66 end
67 67
68 68 def self.fields_for_order_statement(table=nil)
69 69 table ||= table_name
70 70 columns = ['position']
71 71 columns.uniq.map {|field| "#{table}.#{field}"}
72 72 end
73 73
74 74 private
75 75
76 76 def set_position
77 77 max = self.class.where(:custom_field_id => custom_field_id).maximum(:position) || 0
78 78 self.position = max + 1
79 79 end
80 80 end
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CustomFieldValue
19 19 attr_accessor :custom_field, :customized, :value, :value_was
20 20
21 21 def initialize(attributes={})
22 22 attributes.each do |name, v|
23 23 send "#{name}=", v
24 24 end
25 25 end
26 26
27 27 def custom_field_id
28 28 custom_field.id
29 29 end
30 30
31 31 def true?
32 32 self.value == '1'
33 33 end
34 34
35 35 def editable?
36 36 custom_field.editable?
37 37 end
38 38
39 39 def visible?
40 40 custom_field.visible?
41 41 end
42 42
43 43 def required?
44 44 custom_field.is_required?
45 45 end
46 46
47 47 def to_s
48 48 value.to_s
49 49 end
50 50
51 51 def validate_value
52 52 custom_field.validate_custom_value(self).each do |message|
53 53 customized.errors.add(:base, custom_field.name + ' ' + message)
54 54 end
55 55 end
56 56 end
@@ -1,50 +1,50
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 CustomValue < ActiveRecord::Base
19 19 belongs_to :custom_field
20 20 belongs_to :customized, :polymorphic => true
21 21 attr_protected :id
22 22
23 23 def initialize(attributes=nil, *args)
24 24 super
25 25 if new_record? && custom_field && !attributes.key?(:value)
26 26 self.value ||= custom_field.default_value
27 27 end
28 28 end
29 29
30 30 # Returns true if the boolean custom value is true
31 31 def true?
32 32 self.value == '1'
33 33 end
34 34
35 35 def editable?
36 36 custom_field.editable?
37 37 end
38 38
39 39 def visible?
40 40 custom_field.visible?
41 41 end
42 42
43 43 def required?
44 44 custom_field.is_required?
45 45 end
46 46
47 47 def to_s
48 48 value.to_s
49 49 end
50 50 end
@@ -1,75 +1,75
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Document < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :category, :class_name => "DocumentCategory"
22 22 acts_as_attachable :delete_permission => :delete_documents
23 23 acts_as_customizable
24 24
25 25 acts_as_searchable :columns => ['title', "#{table_name}.description"],
26 26 :preload => :project
27 27 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
28 28 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
29 29 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
30 30 acts_as_activity_provider :scope => preload(:project)
31 31
32 32 validates_presence_of :project, :title, :category
33 33 validates_length_of :title, :maximum => 255
34 34 attr_protected :id
35 35
36 36 after_create :send_notification
37 37
38 38 scope :visible, lambda {|*args|
39 39 joins(:project).
40 40 where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
41 41 }
42 42
43 43 safe_attributes 'category_id', 'title', 'description', 'custom_fields', 'custom_field_values'
44 44
45 45 def visible?(user=User.current)
46 46 !user.nil? && user.allowed_to?(:view_documents, project)
47 47 end
48 48
49 49 def initialize(attributes=nil, *args)
50 50 super
51 51 if new_record?
52 52 self.category ||= DocumentCategory.default
53 53 end
54 54 end
55 55
56 56 def updated_on
57 57 unless @updated_on
58 58 a = attachments.last
59 59 @updated_on = (a && a.created_on) || created_on
60 60 end
61 61 @updated_on
62 62 end
63 63
64 64 def notified_users
65 65 project.notified_users.reject {|user| !visible?(user)}
66 66 end
67 67
68 68 private
69 69
70 70 def send_notification
71 71 if Setting.notified_events.include?('document_added')
72 72 Mailer.document_added(self).deliver
73 73 end
74 74 end
75 75 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 DocumentCategory < Enumeration
19 19 has_many :documents, :foreign_key => 'category_id'
20 20
21 21 OptionName = :enumeration_doc_categories
22 22
23 23 def option_name
24 24 OptionName
25 25 end
26 26
27 27 def objects_count
28 28 documents.count
29 29 end
30 30
31 31 def transfer_relations(to)
32 32 documents.update_all(:category_id => to.id)
33 33 end
34 34
35 35 def self.default
36 36 d = super
37 37 d = first if d.nil?
38 38 d
39 39 end
40 40 end
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 DocumentCategoryCustomField < CustomField
19 19 def type_name
20 20 :enumeration_doc_categories
21 21 end
22 22 end
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 DocumentCustomField < CustomField
19 19 def type_name
20 20 :label_document_plural
21 21 end
22 22 end
@@ -1,54 +1,54
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 EmailAddress < ActiveRecord::Base
19 19 belongs_to :user
20 20 attr_protected :id
21 21
22 22 after_update :destroy_tokens
23 23 after_destroy :destroy_tokens
24 24
25 25 validates_presence_of :address
26 26 validates_format_of :address, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
27 27 validates_length_of :address, :maximum => User::MAIL_LENGTH_LIMIT, :allow_nil => true
28 28 validates_uniqueness_of :address, :case_sensitive => false,
29 29 :if => Proc.new {|email| email.address_changed? && email.address.present?}
30 30
31 31 def address=(arg)
32 32 write_attribute(:address, arg.to_s.strip)
33 33 end
34 34
35 35 def destroy
36 36 if is_default?
37 37 false
38 38 else
39 39 super
40 40 end
41 41 end
42 42
43 43 private
44 44
45 45 # Delete all outstanding password reset tokens on email change.
46 46 # This helps to keep the account secure in case the associated email account
47 47 # was compromised.
48 48 def destroy_tokens
49 49 if address_changed? || destroyed?
50 50 tokens = ['recovery']
51 51 Token.where(:user_id => user_id, :action => tokens).delete_all
52 52 end
53 53 end
54 54 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 EnabledModule < ActiveRecord::Base
19 19 belongs_to :project
20 20 acts_as_watchable
21 21
22 22 validates_presence_of :name
23 23 validates_uniqueness_of :name, :scope => :project_id
24 24 attr_protected :id
25 25
26 26 after_create :module_enabled
27 27
28 28 private
29 29
30 30 # after_create callback used to do things when a module is enabled
31 31 def module_enabled
32 32 case name
33 33 when 'wiki'
34 34 # Create a wiki with a default start page
35 35 if project && project.wiki.nil?
36 36 Wiki.create(:project => project, :start_page => 'Wiki')
37 37 end
38 38 end
39 39 end
40 40 end
@@ -1,168 +1,168
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
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 Enumeration < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 default_scope lambda {order(:position)}
22 22
23 23 belongs_to :project
24 24
25 25 acts_as_list :scope => 'type = \'#{type}\' AND #{parent_id ? "parent_id = #{parent_id}" : "parent_id IS NULL"}'
26 26 acts_as_customizable
27 27 acts_as_tree
28 28
29 29 before_destroy :check_integrity
30 30 before_save :check_default
31 31
32 32 attr_protected :type
33 33
34 34 validates_presence_of :name
35 35 validates_uniqueness_of :name, :scope => [:type, :project_id]
36 36 validates_length_of :name, :maximum => 30
37 37
38 38 scope :shared, lambda { where(:project_id => nil) }
39 39 scope :sorted, lambda { order(:position) }
40 40 scope :active, lambda { where(:active => true) }
41 41 scope :system, lambda { where(:project_id => nil) }
42 42 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
43 43
44 44 def self.default
45 45 # Creates a fake default scope so Enumeration.default will check
46 46 # it's type. STI subclasses will automatically add their own
47 47 # types to the finder.
48 48 if self.descends_from_active_record?
49 49 where(:is_default => true, :type => 'Enumeration').first
50 50 else
51 51 # STI classes are
52 52 where(:is_default => true).first
53 53 end
54 54 end
55 55
56 56 # Overloaded on concrete classes
57 57 def option_name
58 58 nil
59 59 end
60 60
61 61 def check_default
62 62 if is_default? && is_default_changed?
63 63 Enumeration.where({:type => type}).update_all({:is_default => false})
64 64 end
65 65 end
66 66
67 67 # Overloaded on concrete classes
68 68 def objects_count
69 69 0
70 70 end
71 71
72 72 def in_use?
73 73 self.objects_count != 0
74 74 end
75 75
76 76 # Is this enumeration overriding a system level enumeration?
77 77 def is_override?
78 78 !self.parent.nil?
79 79 end
80 80
81 81 alias :destroy_without_reassign :destroy
82 82
83 83 # Destroy the enumeration
84 84 # If a enumeration is specified, objects are reassigned
85 85 def destroy(reassign_to = nil)
86 86 if reassign_to && reassign_to.is_a?(Enumeration)
87 87 self.transfer_relations(reassign_to)
88 88 end
89 89 destroy_without_reassign
90 90 end
91 91
92 92 def <=>(enumeration)
93 93 position <=> enumeration.position
94 94 end
95 95
96 96 def to_s; name end
97 97
98 98 # Returns the Subclasses of Enumeration. Each Subclass needs to be
99 99 # required in development mode.
100 100 #
101 101 # Note: subclasses is protected in ActiveRecord
102 102 def self.get_subclasses
103 103 subclasses
104 104 end
105 105
106 106 # Does the +new+ Hash override the previous Enumeration?
107 107 def self.overriding_change?(new, previous)
108 108 if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
109 109 return false
110 110 else
111 111 return true
112 112 end
113 113 end
114 114
115 115 # Does the +new+ Hash have the same custom values as the previous Enumeration?
116 116 def self.same_custom_values?(new, previous)
117 117 previous.custom_field_values.each do |custom_value|
118 118 if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
119 119 return false
120 120 end
121 121 end
122 122
123 123 return true
124 124 end
125 125
126 126 # Are the new and previous fields equal?
127 127 def self.same_active_state?(new, previous)
128 128 new = (new == "1" ? true : false)
129 129 return new == previous
130 130 end
131 131
132 132 # Overrides acts_as_list reset_positions_in_list so that enumeration overrides
133 133 # get the same position as the overriden enumeration
134 134 def reset_positions_in_list
135 135 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
136 136 acts_as_list_class.where("id = :id OR parent_id = :id", :id => item.id).
137 137 update_all({position_column => (i + 1)})
138 138 end
139 139 end
140 140
141 141 private
142 142 def check_integrity
143 143 raise "Cannot delete enumeration" if self.in_use?
144 144 end
145 145
146 146 # Overrides acts_as_list add_to_list_bottom so that enumeration overrides
147 147 # get the same position as the overriden enumeration
148 148 def add_to_list_bottom
149 149 if parent
150 150 self[position_column] = parent.position
151 151 else
152 152 super
153 153 end
154 154 end
155 155
156 156 # Overrides acts_as_list remove_from_list so that enumeration overrides
157 157 # get the same position as the overriden enumeration
158 158 def remove_from_list
159 159 if parent_id.blank?
160 160 super
161 161 end
162 162 end
163 163 end
164 164
165 165 # Force load the subclasses in development mode
166 166 require_dependency 'time_entry_activity'
167 167 require_dependency 'document_category'
168 168 require_dependency 'issue_priority'
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now