##// END OF EJS Templates
update copyright year (#15977)...
Toshi MARUYAMA -
r12461:35cc911192e0
parent child
Show More

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

@@ -1,351 +1,351
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 user = User.find_by_mail(params[:mail].to_s)
86 86 # user not found
87 87 unless user
88 88 flash.now[:error] = l(:notice_account_unknown_email)
89 89 return
90 90 end
91 91 unless user.active?
92 92 handle_inactive_user(user, lost_password_path)
93 93 return
94 94 end
95 95 # user cannot change its password
96 96 unless user.change_password_allowed?
97 97 flash.now[:error] = l(:notice_can_t_change_password)
98 98 return
99 99 end
100 100 # create a new token for password recovery
101 101 token = Token.new(:user => user, :action => "recovery")
102 102 if token.save
103 103 Mailer.lost_password(token).deliver
104 104 flash[:notice] = l(:notice_account_lost_email_sent)
105 105 redirect_to signin_path
106 106 return
107 107 end
108 108 end
109 109 end
110 110 end
111 111
112 112 # User self-registration
113 113 def register
114 114 (redirect_to(home_url); return) unless Setting.self_registration? || session[:auth_source_registration]
115 115 if request.get?
116 116 session[:auth_source_registration] = nil
117 117 @user = User.new(:language => current_language.to_s)
118 118 else
119 119 user_params = params[:user] || {}
120 120 @user = User.new
121 121 @user.safe_attributes = user_params
122 122 @user.admin = false
123 123 @user.register
124 124 if session[:auth_source_registration]
125 125 @user.activate
126 126 @user.login = session[:auth_source_registration][:login]
127 127 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
128 128 if @user.save
129 129 session[:auth_source_registration] = nil
130 130 self.logged_user = @user
131 131 flash[:notice] = l(:notice_account_activated)
132 132 redirect_to my_account_path
133 133 end
134 134 else
135 135 @user.login = params[:user][:login]
136 136 unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
137 137 @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
138 138 end
139 139
140 140 case Setting.self_registration
141 141 when '1'
142 142 register_by_email_activation(@user)
143 143 when '3'
144 144 register_automatically(@user)
145 145 else
146 146 register_manually_by_administrator(@user)
147 147 end
148 148 end
149 149 end
150 150 end
151 151
152 152 # Token based account activation
153 153 def activate
154 154 (redirect_to(home_url); return) unless Setting.self_registration? && params[:token].present?
155 155 token = Token.find_token('register', params[:token].to_s)
156 156 (redirect_to(home_url); return) unless token and !token.expired?
157 157 user = token.user
158 158 (redirect_to(home_url); return) unless user.registered?
159 159 user.activate
160 160 if user.save
161 161 token.destroy
162 162 flash[:notice] = l(:notice_account_activated)
163 163 end
164 164 redirect_to signin_path
165 165 end
166 166
167 167 # Sends a new account activation email
168 168 def activation_email
169 169 if session[:registered_user_id] && Setting.self_registration == '1'
170 170 user_id = session.delete(:registered_user_id).to_i
171 171 user = User.find_by_id(user_id)
172 172 if user && user.registered?
173 173 register_by_email_activation(user)
174 174 return
175 175 end
176 176 end
177 177 redirect_to(home_url)
178 178 end
179 179
180 180 private
181 181
182 182 def authenticate_user
183 183 if Setting.openid? && using_open_id?
184 184 open_id_authenticate(params[:openid_url])
185 185 else
186 186 password_authentication
187 187 end
188 188 end
189 189
190 190 def password_authentication
191 191 user = User.try_to_login(params[:username], params[:password], false)
192 192
193 193 if user.nil?
194 194 invalid_credentials
195 195 elsif user.new_record?
196 196 onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
197 197 else
198 198 # Valid user
199 199 if user.active?
200 200 successful_authentication(user)
201 201 else
202 202 handle_inactive_user(user)
203 203 end
204 204 end
205 205 end
206 206
207 207 def open_id_authenticate(openid_url)
208 208 back_url = signin_url(:autologin => params[:autologin])
209 209 authenticate_with_open_id(
210 210 openid_url, :required => [:nickname, :fullname, :email],
211 211 :return_to => back_url, :method => :post
212 212 ) do |result, identity_url, registration|
213 213 if result.successful?
214 214 user = User.find_or_initialize_by_identity_url(identity_url)
215 215 if user.new_record?
216 216 # Self-registration off
217 217 (redirect_to(home_url); return) unless Setting.self_registration?
218 218 # Create on the fly
219 219 user.login = registration['nickname'] unless registration['nickname'].nil?
220 220 user.mail = registration['email'] unless registration['email'].nil?
221 221 user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
222 222 user.random_password
223 223 user.register
224 224 case Setting.self_registration
225 225 when '1'
226 226 register_by_email_activation(user) do
227 227 onthefly_creation_failed(user)
228 228 end
229 229 when '3'
230 230 register_automatically(user) do
231 231 onthefly_creation_failed(user)
232 232 end
233 233 else
234 234 register_manually_by_administrator(user) do
235 235 onthefly_creation_failed(user)
236 236 end
237 237 end
238 238 else
239 239 # Existing record
240 240 if user.active?
241 241 successful_authentication(user)
242 242 else
243 243 handle_inactive_user(user)
244 244 end
245 245 end
246 246 end
247 247 end
248 248 end
249 249
250 250 def successful_authentication(user)
251 251 logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
252 252 # Valid user
253 253 self.logged_user = user
254 254 # generate a key and set cookie if autologin
255 255 if params[:autologin] && Setting.autologin?
256 256 set_autologin_cookie(user)
257 257 end
258 258 call_hook(:controller_account_success_authentication_after, {:user => user })
259 259 redirect_back_or_default my_page_path
260 260 end
261 261
262 262 def set_autologin_cookie(user)
263 263 token = Token.create(:user => user, :action => 'autologin')
264 264 cookie_options = {
265 265 :value => token.value,
266 266 :expires => 1.year.from_now,
267 267 :path => (Redmine::Configuration['autologin_cookie_path'] || '/'),
268 268 :secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
269 269 :httponly => true
270 270 }
271 271 cookies[autologin_cookie_name] = cookie_options
272 272 end
273 273
274 274 # Onthefly creation failed, display the registration form to fill/fix attributes
275 275 def onthefly_creation_failed(user, auth_source_options = { })
276 276 @user = user
277 277 session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
278 278 render :action => 'register'
279 279 end
280 280
281 281 def invalid_credentials
282 282 logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
283 283 flash.now[:error] = l(:notice_account_invalid_creditentials)
284 284 end
285 285
286 286 # Register a user for email activation.
287 287 #
288 288 # Pass a block for behavior when a user fails to save
289 289 def register_by_email_activation(user, &block)
290 290 token = Token.new(:user => user, :action => "register")
291 291 if user.save and token.save
292 292 Mailer.register(token).deliver
293 293 flash[:notice] = l(:notice_account_register_done, :email => user.mail)
294 294 redirect_to signin_path
295 295 else
296 296 yield if block_given?
297 297 end
298 298 end
299 299
300 300 # Automatically register a user
301 301 #
302 302 # Pass a block for behavior when a user fails to save
303 303 def register_automatically(user, &block)
304 304 # Automatic activation
305 305 user.activate
306 306 user.last_login_on = Time.now
307 307 if user.save
308 308 self.logged_user = user
309 309 flash[:notice] = l(:notice_account_activated)
310 310 redirect_to my_account_path
311 311 else
312 312 yield if block_given?
313 313 end
314 314 end
315 315
316 316 # Manual activation by the administrator
317 317 #
318 318 # Pass a block for behavior when a user fails to save
319 319 def register_manually_by_administrator(user, &block)
320 320 if user.save
321 321 # Sends an email to the administrators
322 322 Mailer.account_activation_request(user).deliver
323 323 account_pending(user)
324 324 else
325 325 yield if block_given?
326 326 end
327 327 end
328 328
329 329 def handle_inactive_user(user, redirect_path=signin_path)
330 330 if user.registered?
331 331 account_pending(user, redirect_path)
332 332 else
333 333 account_locked(user, redirect_path)
334 334 end
335 335 end
336 336
337 337 def account_pending(user, redirect_path=signin_path)
338 338 if Setting.self_registration == '1'
339 339 flash[:error] = l(:notice_account_not_activated_yet, :url => activation_email_path)
340 340 session[:registered_user_id] = user.id
341 341 else
342 342 flash[:error] = l(:notice_account_pending)
343 343 end
344 344 redirect_to redirect_path
345 345 end
346 346
347 347 def account_locked(user, redirect_path=signin_path)
348 348 flash[:error] = l(:notice_account_locked)
349 349 redirect_to redirect_path
350 350 end
351 351 end
@@ -1,75 +1,75
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
34 34
35 35 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
36 36 :with_subprojects => @with_subprojects,
37 37 :author => @author)
38 38 @activity.scope_select {|t| !params["show_#{t}"].nil?}
39 39 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
40 40
41 41 events = @activity.events(@date_from, @date_to)
42 42
43 43 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
44 44 respond_to do |format|
45 45 format.html {
46 46 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
47 47 render :layout => false if request.xhr?
48 48 }
49 49 format.atom {
50 50 title = l(:label_activity)
51 51 if @author
52 52 title = @author.name
53 53 elsif @activity.scope.size == 1
54 54 title = l("label_#{@activity.scope.first.singularize}_plural")
55 55 end
56 56 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
57 57 }
58 58 end
59 59 end
60 60
61 61 rescue ActiveRecord::RecordNotFound
62 62 render_404
63 63 end
64 64
65 65 private
66 66
67 67 # TODO: refactor, duplicated in projects_controller
68 68 def find_optional_project
69 69 return true unless params[:id]
70 70 @project = Project.find(params[:id])
71 71 authorize
72 72 rescue ActiveRecord::RecordNotFound
73 73 render_404
74 74 end
75 75 end
@@ -1,84 +1,84
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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).order('lft')
36 36 scope = scope.like(params[:name]) if params[:name].present?
37 37 @projects = scope.all
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, 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, User.current.mail)
67 67 rescue Exception => e
68 68 flash[:error] = l(:notice_email_error, Redmine::CodesetUtil.replace_invalid_utf8(e.message))
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 [:text_plugin_assets_writable, 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,620 +1,620
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 RoutesHelper
27 27 helper :routes
28 28
29 29 class_attribute :accept_api_auth_actions
30 30 class_attribute :accept_rss_auth_actions
31 31 class_attribute :model_object
32 32
33 33 layout 'base'
34 34
35 35 protect_from_forgery
36 36
37 37 def verify_authenticity_token
38 38 unless api_request?
39 39 super
40 40 end
41 41 end
42 42
43 43 def handle_unverified_request
44 44 unless api_request?
45 45 super
46 46 cookies.delete(autologin_cookie_name)
47 47 render_error :status => 422, :message => "Invalid form authenticity token."
48 48 end
49 49 end
50 50
51 51 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
52 52
53 53 rescue_from ::Unauthorized, :with => :deny_access
54 54 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
55 55
56 56 include Redmine::Search::Controller
57 57 include Redmine::MenuManager::MenuController
58 58 helper Redmine::MenuManager::MenuHelper
59 59
60 60 def session_expiration
61 61 if session[:user_id]
62 62 if session_expired? && !try_to_autologin
63 63 reset_session
64 64 flash[:error] = l(:error_session_expired)
65 65 redirect_to signin_url
66 66 else
67 67 session[:atime] = Time.now.utc.to_i
68 68 end
69 69 end
70 70 end
71 71
72 72 def session_expired?
73 73 if Setting.session_lifetime?
74 74 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
75 75 return true
76 76 end
77 77 end
78 78 if Setting.session_timeout?
79 79 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
80 80 return true
81 81 end
82 82 end
83 83 false
84 84 end
85 85
86 86 def start_user_session(user)
87 87 session[:user_id] = user.id
88 88 session[:ctime] = Time.now.utc.to_i
89 89 session[:atime] = Time.now.utc.to_i
90 90 if user.must_change_password?
91 91 session[:pwd] = '1'
92 92 end
93 93 end
94 94
95 95 def user_setup
96 96 # Check the settings cache for each request
97 97 Setting.check_cache
98 98 # Find the current user
99 99 User.current = find_current_user
100 100 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
101 101 end
102 102
103 103 # Returns the current user or nil if no user is logged in
104 104 # and starts a session if needed
105 105 def find_current_user
106 106 user = nil
107 107 unless api_request?
108 108 if session[:user_id]
109 109 # existing session
110 110 user = (User.active.find(session[:user_id]) rescue nil)
111 111 elsif autologin_user = try_to_autologin
112 112 user = autologin_user
113 113 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
114 114 # RSS key authentication does not start a session
115 115 user = User.find_by_rss_key(params[:key])
116 116 end
117 117 end
118 118 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
119 119 if (key = api_key_from_request)
120 120 # Use API key
121 121 user = User.find_by_api_key(key)
122 122 else
123 123 # HTTP Basic, either username/password or API key/random
124 124 authenticate_with_http_basic do |username, password|
125 125 user = User.try_to_login(username, password) || User.find_by_api_key(username)
126 126 end
127 127 if user && user.must_change_password?
128 128 render_error :message => 'You must change your password', :status => 403
129 129 return
130 130 end
131 131 end
132 132 # Switch user if requested by an admin user
133 133 if user && user.admin? && (username = api_switch_user_from_request)
134 134 su = User.find_by_login(username)
135 135 if su && su.active?
136 136 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
137 137 user = su
138 138 else
139 139 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
140 140 end
141 141 end
142 142 end
143 143 user
144 144 end
145 145
146 146 def autologin_cookie_name
147 147 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
148 148 end
149 149
150 150 def try_to_autologin
151 151 if cookies[autologin_cookie_name] && Setting.autologin?
152 152 # auto-login feature starts a new session
153 153 user = User.try_to_autologin(cookies[autologin_cookie_name])
154 154 if user
155 155 reset_session
156 156 start_user_session(user)
157 157 end
158 158 user
159 159 end
160 160 end
161 161
162 162 # Sets the logged in user
163 163 def logged_user=(user)
164 164 reset_session
165 165 if user && user.is_a?(User)
166 166 User.current = user
167 167 start_user_session(user)
168 168 else
169 169 User.current = User.anonymous
170 170 end
171 171 end
172 172
173 173 # Logs out current user
174 174 def logout_user
175 175 if User.current.logged?
176 176 cookies.delete(autologin_cookie_name)
177 177 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
178 178 self.logged_user = nil
179 179 end
180 180 end
181 181
182 182 # check if login is globally required to access the application
183 183 def check_if_login_required
184 184 # no check needed if user is already logged in
185 185 return true if User.current.logged?
186 186 require_login if Setting.login_required?
187 187 end
188 188
189 189 def check_password_change
190 190 if session[:pwd]
191 191 if User.current.must_change_password?
192 192 redirect_to my_password_path
193 193 else
194 194 session.delete(:pwd)
195 195 end
196 196 end
197 197 end
198 198
199 199 def set_localization
200 200 lang = nil
201 201 if User.current.logged?
202 202 lang = find_language(User.current.language)
203 203 end
204 204 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
205 205 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
206 206 if !accept_lang.blank?
207 207 accept_lang = accept_lang.downcase
208 208 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
209 209 end
210 210 end
211 211 lang ||= Setting.default_language
212 212 set_language_if_valid(lang)
213 213 end
214 214
215 215 def require_login
216 216 if !User.current.logged?
217 217 # Extract only the basic url parameters on non-GET requests
218 218 if request.get?
219 219 url = url_for(params)
220 220 else
221 221 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
222 222 end
223 223 respond_to do |format|
224 224 format.html {
225 225 if request.xhr?
226 226 head :unauthorized
227 227 else
228 228 redirect_to :controller => "account", :action => "login", :back_url => url
229 229 end
230 230 }
231 231 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
232 232 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
233 233 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
234 234 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
235 235 end
236 236 return false
237 237 end
238 238 true
239 239 end
240 240
241 241 def require_admin
242 242 return unless require_login
243 243 if !User.current.admin?
244 244 render_403
245 245 return false
246 246 end
247 247 true
248 248 end
249 249
250 250 def deny_access
251 251 User.current.logged? ? render_403 : require_login
252 252 end
253 253
254 254 # Authorize the user for the requested action
255 255 def authorize(ctrl = params[:controller], action = params[:action], global = false)
256 256 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
257 257 if allowed
258 258 true
259 259 else
260 260 if @project && @project.archived?
261 261 render_403 :message => :notice_not_authorized_archived_project
262 262 else
263 263 deny_access
264 264 end
265 265 end
266 266 end
267 267
268 268 # Authorize the user for the requested action outside a project
269 269 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
270 270 authorize(ctrl, action, global)
271 271 end
272 272
273 273 # Find project of id params[:id]
274 274 def find_project
275 275 @project = Project.find(params[:id])
276 276 rescue ActiveRecord::RecordNotFound
277 277 render_404
278 278 end
279 279
280 280 # Find project of id params[:project_id]
281 281 def find_project_by_project_id
282 282 @project = Project.find(params[:project_id])
283 283 rescue ActiveRecord::RecordNotFound
284 284 render_404
285 285 end
286 286
287 287 # Find a project based on params[:project_id]
288 288 # TODO: some subclasses override this, see about merging their logic
289 289 def find_optional_project
290 290 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
291 291 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
292 292 allowed ? true : deny_access
293 293 rescue ActiveRecord::RecordNotFound
294 294 render_404
295 295 end
296 296
297 297 # Finds and sets @project based on @object.project
298 298 def find_project_from_association
299 299 render_404 unless @object.present?
300 300
301 301 @project = @object.project
302 302 end
303 303
304 304 def find_model_object
305 305 model = self.class.model_object
306 306 if model
307 307 @object = model.find(params[:id])
308 308 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
309 309 end
310 310 rescue ActiveRecord::RecordNotFound
311 311 render_404
312 312 end
313 313
314 314 def self.model_object(model)
315 315 self.model_object = model
316 316 end
317 317
318 318 # Find the issue whose id is the :id parameter
319 319 # Raises a Unauthorized exception if the issue is not visible
320 320 def find_issue
321 321 # Issue.visible.find(...) can not be used to redirect user to the login form
322 322 # if the issue actually exists but requires authentication
323 323 @issue = Issue.find(params[:id])
324 324 raise Unauthorized unless @issue.visible?
325 325 @project = @issue.project
326 326 rescue ActiveRecord::RecordNotFound
327 327 render_404
328 328 end
329 329
330 330 # Find issues with a single :id param or :ids array param
331 331 # Raises a Unauthorized exception if one of the issues is not visible
332 332 def find_issues
333 333 @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
334 334 raise ActiveRecord::RecordNotFound if @issues.empty?
335 335 raise Unauthorized unless @issues.all?(&:visible?)
336 336 @projects = @issues.collect(&:project).compact.uniq
337 337 @project = @projects.first if @projects.size == 1
338 338 rescue ActiveRecord::RecordNotFound
339 339 render_404
340 340 end
341 341
342 342 def find_attachments
343 343 if (attachments = params[:attachments]).present?
344 344 att = attachments.values.collect do |attachment|
345 345 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
346 346 end
347 347 att.compact!
348 348 end
349 349 @attachments = att || []
350 350 end
351 351
352 352 # make sure that the user is a member of the project (or admin) if project is private
353 353 # used as a before_filter for actions that do not require any particular permission on the project
354 354 def check_project_privacy
355 355 if @project && !@project.archived?
356 356 if @project.visible?
357 357 true
358 358 else
359 359 deny_access
360 360 end
361 361 else
362 362 @project = nil
363 363 render_404
364 364 false
365 365 end
366 366 end
367 367
368 368 def back_url
369 369 url = params[:back_url]
370 370 if url.nil? && referer = request.env['HTTP_REFERER']
371 371 url = CGI.unescape(referer.to_s)
372 372 end
373 373 url
374 374 end
375 375
376 376 def redirect_back_or_default(default, options={})
377 377 back_url = params[:back_url].to_s
378 378 if back_url.present?
379 379 begin
380 380 uri = URI.parse(back_url)
381 381 # do not redirect user to another host or to the login or register page
382 382 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
383 383 redirect_to(back_url)
384 384 return
385 385 end
386 386 rescue URI::InvalidURIError
387 387 logger.warn("Could not redirect to invalid URL #{back_url}")
388 388 # redirect to default
389 389 end
390 390 elsif options[:referer]
391 391 redirect_to_referer_or default
392 392 return
393 393 end
394 394 redirect_to default
395 395 false
396 396 end
397 397
398 398 # Redirects to the request referer if present, redirects to args or call block otherwise.
399 399 def redirect_to_referer_or(*args, &block)
400 400 redirect_to :back
401 401 rescue ::ActionController::RedirectBackError
402 402 if args.any?
403 403 redirect_to *args
404 404 elsif block_given?
405 405 block.call
406 406 else
407 407 raise "#redirect_to_referer_or takes arguments or a block"
408 408 end
409 409 end
410 410
411 411 def render_403(options={})
412 412 @project = nil
413 413 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
414 414 return false
415 415 end
416 416
417 417 def render_404(options={})
418 418 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
419 419 return false
420 420 end
421 421
422 422 # Renders an error response
423 423 def render_error(arg)
424 424 arg = {:message => arg} unless arg.is_a?(Hash)
425 425
426 426 @message = arg[:message]
427 427 @message = l(@message) if @message.is_a?(Symbol)
428 428 @status = arg[:status] || 500
429 429
430 430 respond_to do |format|
431 431 format.html {
432 432 render :template => 'common/error', :layout => use_layout, :status => @status
433 433 }
434 434 format.any { head @status }
435 435 end
436 436 end
437 437
438 438 # Handler for ActionView::MissingTemplate exception
439 439 def missing_template
440 440 logger.warn "Missing template, responding with 404"
441 441 @project = nil
442 442 render_404
443 443 end
444 444
445 445 # Filter for actions that provide an API response
446 446 # but have no HTML representation for non admin users
447 447 def require_admin_or_api_request
448 448 return true if api_request?
449 449 if User.current.admin?
450 450 true
451 451 elsif User.current.logged?
452 452 render_error(:status => 406)
453 453 else
454 454 deny_access
455 455 end
456 456 end
457 457
458 458 # Picks which layout to use based on the request
459 459 #
460 460 # @return [boolean, string] name of the layout to use or false for no layout
461 461 def use_layout
462 462 request.xhr? ? false : 'base'
463 463 end
464 464
465 465 def render_feed(items, options={})
466 466 @items = items || []
467 467 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
468 468 @items = @items.slice(0, Setting.feeds_limit.to_i)
469 469 @title = options[:title] || Setting.app_title
470 470 render :template => "common/feed", :formats => [:atom], :layout => false,
471 471 :content_type => 'application/atom+xml'
472 472 end
473 473
474 474 def self.accept_rss_auth(*actions)
475 475 if actions.any?
476 476 self.accept_rss_auth_actions = actions
477 477 else
478 478 self.accept_rss_auth_actions || []
479 479 end
480 480 end
481 481
482 482 def accept_rss_auth?(action=action_name)
483 483 self.class.accept_rss_auth.include?(action.to_sym)
484 484 end
485 485
486 486 def self.accept_api_auth(*actions)
487 487 if actions.any?
488 488 self.accept_api_auth_actions = actions
489 489 else
490 490 self.accept_api_auth_actions || []
491 491 end
492 492 end
493 493
494 494 def accept_api_auth?(action=action_name)
495 495 self.class.accept_api_auth.include?(action.to_sym)
496 496 end
497 497
498 498 # Returns the number of objects that should be displayed
499 499 # on the paginated list
500 500 def per_page_option
501 501 per_page = nil
502 502 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
503 503 per_page = params[:per_page].to_s.to_i
504 504 session[:per_page] = per_page
505 505 elsif session[:per_page]
506 506 per_page = session[:per_page]
507 507 else
508 508 per_page = Setting.per_page_options_array.first || 25
509 509 end
510 510 per_page
511 511 end
512 512
513 513 # Returns offset and limit used to retrieve objects
514 514 # for an API response based on offset, limit and page parameters
515 515 def api_offset_and_limit(options=params)
516 516 if options[:offset].present?
517 517 offset = options[:offset].to_i
518 518 if offset < 0
519 519 offset = 0
520 520 end
521 521 end
522 522 limit = options[:limit].to_i
523 523 if limit < 1
524 524 limit = 25
525 525 elsif limit > 100
526 526 limit = 100
527 527 end
528 528 if offset.nil? && options[:page].present?
529 529 offset = (options[:page].to_i - 1) * limit
530 530 offset = 0 if offset < 0
531 531 end
532 532 offset ||= 0
533 533
534 534 [offset, limit]
535 535 end
536 536
537 537 # qvalues http header parser
538 538 # code taken from webrick
539 539 def parse_qvalues(value)
540 540 tmp = []
541 541 if value
542 542 parts = value.split(/,\s*/)
543 543 parts.each {|part|
544 544 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
545 545 val = m[1]
546 546 q = (m[2] or 1).to_f
547 547 tmp.push([val, q])
548 548 end
549 549 }
550 550 tmp = tmp.sort_by{|val, q| -q}
551 551 tmp.collect!{|val, q| val}
552 552 end
553 553 return tmp
554 554 rescue
555 555 nil
556 556 end
557 557
558 558 # Returns a string that can be used as filename value in Content-Disposition header
559 559 def filename_for_content_disposition(name)
560 560 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
561 561 end
562 562
563 563 def api_request?
564 564 %w(xml json).include? params[:format]
565 565 end
566 566
567 567 # Returns the API key present in the request
568 568 def api_key_from_request
569 569 if params[:key].present?
570 570 params[:key].to_s
571 571 elsif request.headers["X-Redmine-API-Key"].present?
572 572 request.headers["X-Redmine-API-Key"].to_s
573 573 end
574 574 end
575 575
576 576 # Returns the API 'switch user' value if present
577 577 def api_switch_user_from_request
578 578 request.headers["X-Redmine-Switch-User"].to_s.presence
579 579 end
580 580
581 581 # Renders a warning flash if obj has unsaved attachments
582 582 def render_attachment_warning_if_needed(obj)
583 583 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
584 584 end
585 585
586 586 # Rescues an invalid query statement. Just in case...
587 587 def query_statement_invalid(exception)
588 588 logger.error "Query::StatementInvalid: #{exception.message}" if logger
589 589 session.delete(:query)
590 590 sort_clear if respond_to?(:sort_clear)
591 591 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
592 592 end
593 593
594 594 # Renders a 200 response for successfull updates or deletions via the API
595 595 def render_api_ok
596 596 render_api_head :ok
597 597 end
598 598
599 599 # Renders a head API response
600 600 def render_api_head(status)
601 601 # #head would return a response body with one space
602 602 render :text => '', :status => status, :layout => nil
603 603 end
604 604
605 605 # Renders API response on validation failure
606 606 def render_validation_errors(objects)
607 607 if objects.is_a?(Array)
608 608 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
609 609 else
610 610 @error_messages = objects.errors.full_messages
611 611 end
612 612 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
613 613 end
614 614
615 615 # Overrides #_include_layout? so that #render with no arguments
616 616 # doesn't use the layout for api requests
617 617 def _include_layout?(*args)
618 618 api_request? ? false : super
619 619 end
620 620 end
@@ -1,154 +1,154
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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_project, :except => :upload
20 20 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 21 before_filter :delete_authorize, :only => :destroy
22 22 before_filter :authorize_global, :only => :upload
23 23
24 24 accept_api_auth :show, :download, :upload
25 25
26 26 def show
27 27 respond_to do |format|
28 28 format.html {
29 29 if @attachment.is_diff?
30 30 @diff = File.new(@attachment.diskfile, "rb").read
31 31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 33 # Save diff type as user preference
34 34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 35 User.current.pref[:diff_type] = @diff_type
36 36 User.current.preference.save
37 37 end
38 38 render :action => 'diff'
39 39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 40 @content = File.new(@attachment.diskfile, "rb").read
41 41 render :action => 'file'
42 42 else
43 43 download
44 44 end
45 45 }
46 46 format.api
47 47 end
48 48 end
49 49
50 50 def download
51 51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
52 52 @attachment.increment_download
53 53 end
54 54
55 55 if stale?(:etag => @attachment.digest)
56 56 # images are sent inline
57 57 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
58 58 :type => detect_content_type(@attachment),
59 59 :disposition => (@attachment.image? ? 'inline' : 'attachment')
60 60 end
61 61 end
62 62
63 63 def thumbnail
64 64 if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
65 65 if stale?(:etag => thumbnail)
66 66 send_file thumbnail,
67 67 :filename => filename_for_content_disposition(@attachment.filename),
68 68 :type => detect_content_type(@attachment),
69 69 :disposition => 'inline'
70 70 end
71 71 else
72 72 # No thumbnail for the attachment or thumbnail could not be created
73 73 render :nothing => true, :status => 404
74 74 end
75 75 end
76 76
77 77 def upload
78 78 # Make sure that API users get used to set this content type
79 79 # as it won't trigger Rails' automatic parsing of the request body for parameters
80 80 unless request.content_type == 'application/octet-stream'
81 81 render :nothing => true, :status => 406
82 82 return
83 83 end
84 84
85 85 @attachment = Attachment.new(:file => request.raw_post)
86 86 @attachment.author = User.current
87 87 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
88 88 saved = @attachment.save
89 89
90 90 respond_to do |format|
91 91 format.js
92 92 format.api {
93 93 if saved
94 94 render :action => 'upload', :status => :created
95 95 else
96 96 render_validation_errors(@attachment)
97 97 end
98 98 }
99 99 end
100 100 end
101 101
102 102 def destroy
103 103 if @attachment.container.respond_to?(:init_journal)
104 104 @attachment.container.init_journal(User.current)
105 105 end
106 106 if @attachment.container
107 107 # Make sure association callbacks are called
108 108 @attachment.container.attachments.delete(@attachment)
109 109 else
110 110 @attachment.destroy
111 111 end
112 112
113 113 respond_to do |format|
114 114 format.html { redirect_to_referer_or project_path(@project) }
115 115 format.js
116 116 end
117 117 end
118 118
119 119 private
120 120 def find_project
121 121 @attachment = Attachment.find(params[:id])
122 122 # Show 404 if the filename in the url is wrong
123 123 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
124 124 @project = @attachment.project
125 125 rescue ActiveRecord::RecordNotFound
126 126 render_404
127 127 end
128 128
129 129 # Checks that the file exists and is readable
130 130 def file_readable
131 131 if @attachment.readable?
132 132 true
133 133 else
134 134 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
135 135 render_404
136 136 end
137 137 end
138 138
139 139 def read_authorize
140 140 @attachment.visible? ? true : deny_access
141 141 end
142 142
143 143 def delete_authorize
144 144 @attachment.deletable? ? true : deny_access
145 145 end
146 146
147 147 def detect_content_type(attachment)
148 148 content_type = attachment.content_type
149 149 if content_type.blank?
150 150 content_type = Redmine::MimeType.of(attachment.filename)
151 151 end
152 152 content_type.to_s
153 153 end
154 154 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
25 25 def index
26 26 @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
27 27 end
28 28
29 29 def new
30 30 klass_name = params[:type] || 'AuthSourceLdap'
31 31 @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
32 32 render_404 unless @auth_source
33 33 end
34 34
35 35 def create
36 36 @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
37 37 if @auth_source.save
38 38 flash[:notice] = l(:notice_successful_create)
39 39 redirect_to auth_sources_path
40 40 else
41 41 render :action => 'new'
42 42 end
43 43 end
44 44
45 45 def edit
46 46 end
47 47
48 48 def update
49 49 if @auth_source.update_attributes(params[:auth_source])
50 50 flash[:notice] = l(:notice_successful_update)
51 51 redirect_to auth_sources_path
52 52 else
53 53 render :action => 'edit'
54 54 end
55 55 end
56 56
57 57 def test_connection
58 58 begin
59 59 @auth_source.test_connection
60 60 flash[:notice] = l(:notice_successful_connection)
61 61 rescue Exception => e
62 62 flash[:error] = l(:error_unable_to_connect, e.message)
63 63 end
64 64 redirect_to auth_sources_path
65 65 end
66 66
67 67 def destroy
68 68 unless @auth_source.users.exists?
69 69 @auth_source.destroy
70 70 flash[:notice] = l(:notice_successful_delete)
71 71 end
72 72 redirect_to auth_sources_path
73 73 end
74 74
75 75 def autocomplete_for_new_user
76 76 results = AuthSource.search(params[:term])
77 77
78 78 render :json => results.map {|result| {
79 79 'value' => result[:login],
80 80 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})",
81 81 'login' => result[:login].to_s,
82 82 'firstname' => result[:firstname].to_s,
83 83 'lastname' => result[:lastname].to_s,
84 84 'mail' => result[:mail].to_s,
85 85 'auth_source_id' => result[:auth_source_id].to_s
86 86 }}
87 87 end
88 88
89 89 private
90 90
91 91 def find_auth_source
92 92 @auth_source = AuthSource.find(params[:id])
93 93 rescue ActiveRecord::RecordNotFound
94 94 render_404
95 95 end
96 96 end
@@ -1,44 +1,44
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 = (params[:scope] == "all" || @project.nil? ? Issue : @project.issues).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).all
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-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.includes(:project, :last_message => :author).all
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 includes(:last_reply).
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 all
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 all
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-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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,84 +1,84
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
35 35 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
36 36 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
37 37 :delete => User.current.allowed_to?(:delete_issues, @projects)
38 38 }
39 39 if @project
40 40 if @issue
41 41 @assignables = @issue.assignable_users
42 42 else
43 43 @assignables = @project.assignable_users
44 44 end
45 45 @trackers = @project.trackers
46 46 else
47 47 #when multiple projects, we only keep the intersection of each set
48 48 @assignables = @projects.map(&:assignable_users).reduce(:&)
49 49 @trackers = @projects.map(&:trackers).reduce(:&)
50 50 end
51 51 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
52 52
53 53 @priorities = IssuePriority.active.reverse
54 54 @back = back_url
55 55
56 56 @options_by_custom_field = {}
57 57 if @can[:edit]
58 58 custom_fields = @issues.map(&:available_custom_fields).reduce(:&).reject(&:multiple?)
59 59 custom_fields.each do |field|
60 60 values = field.possible_values_options(@projects)
61 61 if values.present?
62 62 @options_by_custom_field[field] = values
63 63 end
64 64 end
65 65 end
66 66
67 67 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
68 68 render :layout => false
69 69 end
70 70
71 71 def time_entries
72 72 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
73 73 (render_404; return) unless @time_entries.present?
74 74
75 75 @projects = @time_entries.collect(&:project).compact.uniq
76 76 @project = @projects.first if @projects.size == 1
77 77 @activities = TimeEntryActivity.shared.active
78 78 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
79 79 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
80 80 }
81 81 @back = back_url
82 82 render :layout => false
83 83 end
84 84 end
@@ -1,89 +1,89
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 @tab = params[:tab] || 'IssueCustomField'
31 31 }
32 32 format.api {
33 33 @custom_fields = CustomField.all
34 34 }
35 35 end
36 36 end
37 37
38 38 def new
39 39 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
40 40 @custom_field.default_value = nil
41 41 end
42 42
43 43 def create
44 44 if @custom_field.save
45 45 flash[:notice] = l(:notice_successful_create)
46 46 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
47 47 redirect_to custom_fields_path(:tab => @custom_field.class.name)
48 48 else
49 49 render :action => 'new'
50 50 end
51 51 end
52 52
53 53 def edit
54 54 end
55 55
56 56 def update
57 57 if @custom_field.update_attributes(params[:custom_field])
58 58 flash[:notice] = l(:notice_successful_update)
59 59 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
60 60 redirect_to custom_fields_path(:tab => @custom_field.class.name)
61 61 else
62 62 render :action => 'edit'
63 63 end
64 64 end
65 65
66 66 def destroy
67 67 begin
68 68 @custom_field.destroy
69 69 rescue
70 70 flash[:error] = l(:error_can_not_delete_custom_field)
71 71 end
72 72 redirect_to custom_fields_path(:tab => @custom_field.class.name)
73 73 end
74 74
75 75 private
76 76
77 77 def build_new_custom_field
78 78 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
79 79 if @custom_field.nil?
80 80 render_404
81 81 end
82 82 end
83 83
84 84 def find_custom_field
85 85 @custom_field = CustomField.find(params[:id])
86 86 rescue ActiveRecord::RecordNotFound
87 87 render_404
88 88 end
89 89 end
@@ -1,94 +1,94
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
28 28 def index
29 29 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
30 30 documents = @project.documents.includes(:attachments, :category).all
31 31 case @sort_by
32 32 when 'date'
33 33 @grouped = documents.group_by {|d| d.updated_on.to_date }
34 34 when 'title'
35 35 @grouped = documents.group_by {|d| d.title.first.upcase}
36 36 when 'author'
37 37 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
38 38 else
39 39 @grouped = documents.group_by(&:category)
40 40 end
41 41 @document = @project.documents.build
42 42 render :layout => false if request.xhr?
43 43 end
44 44
45 45 def show
46 46 @attachments = @document.attachments.all
47 47 end
48 48
49 49 def new
50 50 @document = @project.documents.build
51 51 @document.safe_attributes = params[:document]
52 52 end
53 53
54 54 def create
55 55 @document = @project.documents.build
56 56 @document.safe_attributes = params[:document]
57 57 @document.save_attachments(params[:attachments])
58 58 if @document.save
59 59 render_attachment_warning_if_needed(@document)
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to project_documents_path(@project)
62 62 else
63 63 render :action => 'new'
64 64 end
65 65 end
66 66
67 67 def edit
68 68 end
69 69
70 70 def update
71 71 @document.safe_attributes = params[:document]
72 72 if request.put? and @document.save
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to document_path(@document)
75 75 else
76 76 render :action => 'edit'
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @document.destroy if request.delete?
82 82 redirect_to project_documents_path(@project)
83 83 end
84 84
85 85 def add_attachment
86 86 attachments = Attachment.attach_files(@document, params[:attachments])
87 87 render_attachment_warning_if_needed(@document)
88 88
89 89 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
90 90 Mailer.attachments_added(attachments[:files]).deliver
91 91 end
92 92 redirect_to document_path(@document)
93 93 end
94 94 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.all
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 request.put? && @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.all - [@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,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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).reorder(sort_clause).find(@project.id)]
35 35 @containers += @project.versions.includes(:attachments).reorder(sort_clause).all.sort.reverse
36 36 render :layout => !request.xhr?
37 37 end
38 38
39 39 def new
40 40 @versions = @project.versions.sort
41 41 end
42 42
43 43 def create
44 44 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
45 45 attachments = Attachment.attach_files(container, params[:attachments])
46 46 render_attachment_warning_if_needed(container)
47 47
48 48 if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
49 49 Mailer.attachments_added(attachments[:files]).deliver
50 50 end
51 51 redirect_to project_files_path(@project)
52 52 end
53 53 end
@@ -1,48 +1,48
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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,141 +1,141
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 helper :custom_fields
26 26
27 27 def index
28 28 @groups = Group.sorted.all
29 29
30 30 respond_to do |format|
31 31 format.html
32 32 format.api
33 33 end
34 34 end
35 35
36 36 def show
37 37 respond_to do |format|
38 38 format.html
39 39 format.api
40 40 end
41 41 end
42 42
43 43 def new
44 44 @group = Group.new
45 45 end
46 46
47 47 def create
48 48 @group = Group.new
49 49 @group.safe_attributes = params[:group]
50 50
51 51 respond_to do |format|
52 52 if @group.save
53 53 format.html {
54 54 flash[:notice] = l(:notice_successful_create)
55 55 redirect_to(params[:continue] ? new_group_path : groups_path)
56 56 }
57 57 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
58 58 else
59 59 format.html { render :action => "new" }
60 60 format.api { render_validation_errors(@group) }
61 61 end
62 62 end
63 63 end
64 64
65 65 def edit
66 66 end
67 67
68 68 def update
69 69 @group.safe_attributes = params[:group]
70 70
71 71 respond_to do |format|
72 72 if @group.save
73 73 flash[:notice] = l(:notice_successful_update)
74 74 format.html { redirect_to(groups_path) }
75 75 format.api { render_api_ok }
76 76 else
77 77 format.html { render :action => "edit" }
78 78 format.api { render_validation_errors(@group) }
79 79 end
80 80 end
81 81 end
82 82
83 83 def destroy
84 84 @group.destroy
85 85
86 86 respond_to do |format|
87 87 format.html { redirect_to(groups_path) }
88 88 format.api { render_api_ok }
89 89 end
90 90 end
91 91
92 92 def add_users
93 93 @users = User.where(:id => (params[:user_id] || params[:user_ids])).all
94 94 @group.users << @users if request.post?
95 95 respond_to do |format|
96 96 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
97 97 format.js
98 98 format.api { render_api_ok }
99 99 end
100 100 end
101 101
102 102 def remove_user
103 103 @group.users.delete(User.find(params[:user_id])) if request.delete?
104 104 respond_to do |format|
105 105 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
106 106 format.js
107 107 format.api { render_api_ok }
108 108 end
109 109 end
110 110
111 111 def autocomplete_for_user
112 112 respond_to do |format|
113 113 format.js
114 114 end
115 115 end
116 116
117 117 def edit_membership
118 118 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
119 119 @membership.save if request.post?
120 120 respond_to do |format|
121 121 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
122 122 format.js
123 123 end
124 124 end
125 125
126 126 def destroy_membership
127 127 Member.find(params[:membership_id]).destroy if request.post?
128 128 respond_to do |format|
129 129 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
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 end
@@ -1,122 +1,122
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.all }
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,88 +1,88
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
20 20 before_filter :find_relation, :except => [:index, :create]
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 saved = @relation.save
49 49
50 50 respond_to do |format|
51 51 format.html { redirect_to issue_path(@issue) }
52 52 format.js {
53 53 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
54 54 }
55 55 format.api {
56 56 if saved
57 57 render :action => 'show', :status => :created, :location => relation_url(@relation)
58 58 else
59 59 render_validation_errors(@relation)
60 60 end
61 61 }
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 raise Unauthorized unless @relation.deletable?
67 67 @relation.destroy
68 68
69 69 respond_to do |format|
70 70 format.html { redirect_to issue_path(@relation.issue_from) }
71 71 format.js
72 72 format.api { render_api_ok }
73 73 end
74 74 end
75 75
76 76 private
77 77 def find_issue
78 78 @issue = @object = Issue.find(params[:issue_id])
79 79 rescue ActiveRecord::RecordNotFound
80 80 render_404
81 81 end
82 82
83 83 def find_relation
84 84 @relation = IssueRelation.find(params[:id])
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88 end
@@ -1,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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').all
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
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,481 +1,481
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :find_project, :only => [:new, :create, :update_form]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 28 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
29 29 accept_rss_auth :index, :show
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 helper :sort
50 50 include SortHelper
51 51 include IssuesHelper
52 52 helper :timelog
53 53 include Redmine::Export::PDF
54 54
55 55 def index
56 56 retrieve_query
57 57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 58 sort_update(@query.sortable_columns)
59 59 @query.sort_criteria = sort_criteria.to_a
60 60
61 61 if @query.valid?
62 62 case params[:format]
63 63 when 'csv', 'pdf'
64 64 @limit = Setting.issues_export_limit.to_i
65 65 when 'atom'
66 66 @limit = Setting.feeds_limit.to_i
67 67 when 'xml', 'json'
68 68 @offset, @limit = api_offset_and_limit
69 69 else
70 70 @limit = per_page_option
71 71 end
72 72
73 73 @issue_count = @query.issue_count
74 74 @issue_pages = Paginator.new @issue_count, @limit, params['page']
75 75 @offset ||= @issue_pages.offset
76 76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
77 77 :order => sort_clause,
78 78 :offset => @offset,
79 79 :limit => @limit)
80 80 @issue_count_by_group = @query.issue_count_by_group
81 81
82 82 respond_to do |format|
83 83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
84 84 format.api {
85 85 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
86 86 }
87 87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
88 88 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
89 89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
90 90 end
91 91 else
92 92 respond_to do |format|
93 93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
94 94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
95 95 format.api { render_validation_errors(@query) }
96 96 end
97 97 end
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101
102 102 def show
103 103 @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
104 104 @journals.each_with_index {|j,i| j.indice = i+1}
105 105 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
106 106 Journal.preload_journals_details_custom_fields(@journals)
107 107 # TODO: use #select! when ruby1.8 support is dropped
108 108 @journals.reject! {|journal| !journal.notes? && journal.visible_details.empty?}
109 109 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 110
111 111 @changesets = @issue.changesets.visible.all
112 112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 113
114 114 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
115 115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 117 @priorities = IssuePriority.active
118 118 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
119 119 @relation = IssueRelation.new
120 120
121 121 respond_to do |format|
122 122 format.html {
123 123 retrieve_previous_and_next_issue_ids
124 124 render :template => 'issues/show'
125 125 }
126 126 format.api
127 127 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
128 128 format.pdf {
129 129 pdf = issue_to_pdf(@issue, :journals => @journals)
130 130 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
131 131 }
132 132 end
133 133 end
134 134
135 135 # Add a new issue
136 136 # The new issue will be created from an existing one if copy_from parameter is given
137 137 def new
138 138 respond_to do |format|
139 139 format.html { render :action => 'new', :layout => !request.xhr? }
140 140 end
141 141 end
142 142
143 143 def create
144 144 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
145 145 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
146 146 if @issue.save
147 147 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
148 148 respond_to do |format|
149 149 format.html {
150 150 render_attachment_warning_if_needed(@issue)
151 151 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
152 152 if params[:continue]
153 153 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
154 154 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
155 155 else
156 156 redirect_to issue_path(@issue)
157 157 end
158 158 }
159 159 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
160 160 end
161 161 return
162 162 else
163 163 respond_to do |format|
164 164 format.html { render :action => 'new' }
165 165 format.api { render_validation_errors(@issue) }
166 166 end
167 167 end
168 168 end
169 169
170 170 def edit
171 171 return unless update_issue_from_params
172 172
173 173 respond_to do |format|
174 174 format.html { }
175 175 format.xml { }
176 176 end
177 177 end
178 178
179 179 def update
180 180 return unless update_issue_from_params
181 181 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
182 182 saved = false
183 183 begin
184 184 saved = save_issue_with_child_records
185 185 rescue ActiveRecord::StaleObjectError
186 186 @conflict = true
187 187 if params[:last_journal_id]
188 188 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
189 189 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
190 190 end
191 191 end
192 192
193 193 if saved
194 194 render_attachment_warning_if_needed(@issue)
195 195 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
196 196
197 197 respond_to do |format|
198 198 format.html { redirect_back_or_default issue_path(@issue) }
199 199 format.api { render_api_ok }
200 200 end
201 201 else
202 202 respond_to do |format|
203 203 format.html { render :action => 'edit' }
204 204 format.api { render_validation_errors(@issue) }
205 205 end
206 206 end
207 207 end
208 208
209 209 # Updates the issue form when changing the project, status or tracker
210 210 # on issue creation/update
211 211 def update_form
212 212 end
213 213
214 214 # Bulk edit/copy a set of issues
215 215 def bulk_edit
216 216 @issues.sort!
217 217 @copy = params[:copy].present?
218 218 @notes = params[:notes]
219 219
220 220 if User.current.allowed_to?(:move_issues, @projects)
221 221 @allowed_projects = Issue.allowed_target_projects_on_move
222 222 if params[:issue]
223 223 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
224 224 if @target_project
225 225 target_projects = [@target_project]
226 226 end
227 227 end
228 228 end
229 229 target_projects ||= @projects
230 230
231 231 if @copy
232 232 @available_statuses = [IssueStatus.default]
233 233 else
234 234 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
235 235 end
236 236 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
237 237 @assignables = target_projects.map(&:assignable_users).reduce(:&)
238 238 @trackers = target_projects.map(&:trackers).reduce(:&)
239 239 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
240 240 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
241 241 if @copy
242 242 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
243 243 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
244 244 end
245 245
246 246 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
247 247
248 248 @issue_params = params[:issue] || {}
249 249 @issue_params[:custom_field_values] ||= {}
250 250 end
251 251
252 252 def bulk_update
253 253 @issues.sort!
254 254 @copy = params[:copy].present?
255 255 attributes = parse_params_for_bulk_issue_attributes(params)
256 256
257 257 unsaved_issues = []
258 258 saved_issues = []
259 259
260 260 if @copy && params[:copy_subtasks].present?
261 261 # Descendant issues will be copied with the parent task
262 262 # Don't copy them twice
263 263 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
264 264 end
265 265
266 266 @issues.each do |orig_issue|
267 267 orig_issue.reload
268 268 if @copy
269 269 issue = orig_issue.copy({},
270 270 :attachments => params[:copy_attachments].present?,
271 271 :subtasks => params[:copy_subtasks].present?
272 272 )
273 273 else
274 274 issue = orig_issue
275 275 end
276 276 journal = issue.init_journal(User.current, params[:notes])
277 277 issue.safe_attributes = attributes
278 278 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
279 279 if issue.save
280 280 saved_issues << issue
281 281 else
282 282 unsaved_issues << orig_issue
283 283 end
284 284 end
285 285
286 286 if unsaved_issues.empty?
287 287 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
288 288 if params[:follow]
289 289 if @issues.size == 1 && saved_issues.size == 1
290 290 redirect_to issue_path(saved_issues.first)
291 291 elsif saved_issues.map(&:project).uniq.size == 1
292 292 redirect_to project_issues_path(saved_issues.map(&:project).first)
293 293 end
294 294 else
295 295 redirect_back_or_default _project_issues_path(@project)
296 296 end
297 297 else
298 298 @saved_issues = @issues
299 299 @unsaved_issues = unsaved_issues
300 300 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).all
301 301 bulk_edit
302 302 render :action => 'bulk_edit'
303 303 end
304 304 end
305 305
306 306 def destroy
307 307 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
308 308 if @hours > 0
309 309 case params[:todo]
310 310 when 'destroy'
311 311 # nothing to do
312 312 when 'nullify'
313 313 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
314 314 when 'reassign'
315 315 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
316 316 if reassign_to.nil?
317 317 flash.now[:error] = l(:error_issue_not_found_in_project)
318 318 return
319 319 else
320 320 TimeEntry.where(['issue_id IN (?)', @issues]).
321 321 update_all("issue_id = #{reassign_to.id}")
322 322 end
323 323 else
324 324 # display the destroy form if it's a user request
325 325 return unless api_request?
326 326 end
327 327 end
328 328 @issues.each do |issue|
329 329 begin
330 330 issue.reload.destroy
331 331 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
332 332 # nothing to do, issue was already deleted (eg. by a parent)
333 333 end
334 334 end
335 335 respond_to do |format|
336 336 format.html { redirect_back_or_default _project_issues_path(@project) }
337 337 format.api { render_api_ok }
338 338 end
339 339 end
340 340
341 341 private
342 342
343 343 def find_project
344 344 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
345 345 @project = Project.find(project_id)
346 346 rescue ActiveRecord::RecordNotFound
347 347 render_404
348 348 end
349 349
350 350 def retrieve_previous_and_next_issue_ids
351 351 retrieve_query_from_session
352 352 if @query
353 353 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
354 354 sort_update(@query.sortable_columns, 'issues_index_sort')
355 355 limit = 500
356 356 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
357 357 if (idx = issue_ids.index(@issue.id)) && idx < limit
358 358 if issue_ids.size < 500
359 359 @issue_position = idx + 1
360 360 @issue_count = issue_ids.size
361 361 end
362 362 @prev_issue_id = issue_ids[idx - 1] if idx > 0
363 363 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
364 364 end
365 365 end
366 366 end
367 367
368 368 # Used by #edit and #update to set some common instance variables
369 369 # from the params
370 370 # TODO: Refactor, not everything in here is needed by #edit
371 371 def update_issue_from_params
372 372 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
373 373 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
374 374 @time_entry.attributes = params[:time_entry]
375 375
376 376 @issue.init_journal(User.current)
377 377
378 378 issue_attributes = params[:issue]
379 379 if issue_attributes && params[:conflict_resolution]
380 380 case params[:conflict_resolution]
381 381 when 'overwrite'
382 382 issue_attributes = issue_attributes.dup
383 383 issue_attributes.delete(:lock_version)
384 384 when 'add_notes'
385 385 issue_attributes = issue_attributes.slice(:notes)
386 386 when 'cancel'
387 387 redirect_to issue_path(@issue)
388 388 return false
389 389 end
390 390 end
391 391 @issue.safe_attributes = issue_attributes
392 392 @priorities = IssuePriority.active
393 393 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
394 394 true
395 395 end
396 396
397 397 # TODO: Refactor, lots of extra code in here
398 398 # TODO: Changing tracker on an existing issue should not trigger this
399 399 def build_new_issue_from_params
400 400 if params[:id].blank?
401 401 @issue = Issue.new
402 402 if params[:copy_from]
403 403 begin
404 404 @copy_from = Issue.visible.find(params[:copy_from])
405 405 @copy_attachments = params[:copy_attachments].present? || request.get?
406 406 @copy_subtasks = params[:copy_subtasks].present? || request.get?
407 407 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
408 408 rescue ActiveRecord::RecordNotFound
409 409 render_404
410 410 return
411 411 end
412 412 end
413 413 @issue.project = @project
414 414 else
415 415 @issue = @project.issues.visible.find(params[:id])
416 416 end
417 417
418 418 @issue.project = @project
419 419 @issue.author ||= User.current
420 420 # Tracker must be set before custom field values
421 421 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
422 422 if @issue.tracker.nil?
423 423 render_error l(:error_no_tracker_in_project)
424 424 return false
425 425 end
426 426 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
427 427 @issue.safe_attributes = params[:issue]
428 428
429 429 @priorities = IssuePriority.active
430 430 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
431 431 @available_watchers = @issue.watcher_users
432 432 if @issue.project.users.count <= 20
433 433 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
434 434 end
435 435 end
436 436
437 437 def check_for_default_issue_status
438 438 if IssueStatus.default.nil?
439 439 render_error l(:error_no_default_issue_status)
440 440 return false
441 441 end
442 442 end
443 443
444 444 def parse_params_for_bulk_issue_attributes(params)
445 445 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
446 446 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
447 447 if custom = attributes[:custom_field_values]
448 448 custom.reject! {|k,v| v.blank?}
449 449 custom.keys.each do |k|
450 450 if custom[k].is_a?(Array)
451 451 custom[k] << '' if custom[k].delete('__none__')
452 452 else
453 453 custom[k] = '' if custom[k] == '__none__'
454 454 end
455 455 end
456 456 end
457 457 attributes
458 458 end
459 459
460 460 # Saves @issue and a time_entry from the parameters
461 461 def save_issue_with_child_records
462 462 Issue.transaction do
463 463 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
464 464 time_entry = @time_entry || TimeEntry.new
465 465 time_entry.project = @issue.project
466 466 time_entry.issue = @issue
467 467 time_entry.user = User.current
468 468 time_entry.spent_on = User.current.today
469 469 time_entry.attributes = params[:time_entry]
470 470 @issue.time_entries << time_entry
471 471 end
472 472
473 473 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
474 474 if @issue.save
475 475 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
476 476 else
477 477 raise ActiveRecord::Rollback
478 478 end
479 479 end
480 480 end
481 481 end
@@ -1,105 +1,105
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
38 38 if @query.valid?
39 39 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
40 40 :limit => 25)
41 41 end
42 42 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
43 43 render :layout => false, :content_type => 'application/atom+xml'
44 44 rescue ActiveRecord::RecordNotFound
45 45 render_404
46 46 end
47 47
48 48 def diff
49 49 @issue = @journal.issue
50 50 if params[:detail_id].present?
51 51 @detail = @journal.details.find_by_id(params[:detail_id])
52 52 else
53 53 @detail = @journal.details.detect {|d| d.prop_key == 'description'}
54 54 end
55 55 (render_404; return false) unless @issue && @detail
56 56 @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
57 57 end
58 58
59 59 def new
60 60 @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
61 61 if @journal
62 62 user = @journal.user
63 63 text = @journal.notes
64 64 else
65 65 user = @issue.author
66 66 text = @issue.description
67 67 end
68 68 # Replaces pre blocks with [...]
69 69 text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
70 70 @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
71 71 @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
72 72 rescue ActiveRecord::RecordNotFound
73 73 render_404
74 74 end
75 75
76 76 def edit
77 77 (render_403; return false) unless @journal.editable_by?(User.current)
78 78 if request.post?
79 79 @journal.update_attributes(:notes => params[:notes]) if params[:notes]
80 80 @journal.destroy if @journal.details.empty? && @journal.notes.blank?
81 81 call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
82 82 respond_to do |format|
83 83 format.html { redirect_to issue_path(@journal.journalized) }
84 84 format.js { render :action => 'update' }
85 85 end
86 86 else
87 87 respond_to do |format|
88 88 format.html {
89 89 # TODO: implement non-JS journal update
90 90 render :nothing => true
91 91 }
92 92 format.js
93 93 end
94 94 end
95 95 end
96 96
97 97 private
98 98
99 99 def find_journal
100 100 @journal = Journal.visible.find(params[:id])
101 101 @project = @journal.journalized.project
102 102 rescue ActiveRecord::RecordNotFound
103 103 render_404
104 104 end
105 105 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 # Submits an incoming email to MailHandler
22 22 def index
23 23 options = params.dup
24 24 email = options.delete(:email)
25 25 if MailHandler.receive(email, options)
26 26 render :nothing => true, :status => :created
27 27 else
28 28 render :nothing => true, :status => :unprocessable_entity
29 29 end
30 30 end
31 31
32 32 private
33 33
34 34 def check_credential
35 35 User.current = nil
36 36 unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
37 37 render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
38 38 end
39 39 end
40 40 end
@@ -1,124 +1,124
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
23 23 before_filter :authorize
24 24 accept_api_auth :index, :show, :create, :update, :destroy
25 25
26 26 def index
27 27 @offset, @limit = api_offset_and_limit
28 28 @member_count = @project.member_principals.count
29 29 @member_pages = Paginator.new @member_count, @limit, params['page']
30 30 @offset ||= @member_pages.offset
31 31 @members = @project.member_principals.
32 32 order("#{Member.table_name}.id").
33 33 limit(@limit).
34 34 offset(@offset).
35 35 all
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 create
50 50 members = []
51 51 if params[:membership]
52 52 if params[:membership][:user_ids]
53 53 attrs = params[:membership].dup
54 54 user_ids = attrs.delete(:user_ids)
55 55 user_ids.each do |user_id|
56 56 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
57 57 end
58 58 else
59 59 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
60 60 end
61 61 @project.members << members
62 62 end
63 63
64 64 respond_to do |format|
65 65 format.html { redirect_to_settings_in_projects }
66 66 format.js { @members = members }
67 67 format.api {
68 68 @member = members.first
69 69 if @member.valid?
70 70 render :action => 'show', :status => :created, :location => membership_url(@member)
71 71 else
72 72 render_validation_errors(@member)
73 73 end
74 74 }
75 75 end
76 76 end
77 77
78 78 def update
79 79 if params[:membership]
80 80 @member.role_ids = params[:membership][:role_ids]
81 81 end
82 82 saved = @member.save
83 83 respond_to do |format|
84 84 format.html { redirect_to_settings_in_projects }
85 85 format.js
86 86 format.api {
87 87 if saved
88 88 render_api_ok
89 89 else
90 90 render_validation_errors(@member)
91 91 end
92 92 }
93 93 end
94 94 end
95 95
96 96 def destroy
97 97 if request.delete? && @member.deletable?
98 98 @member.destroy
99 99 end
100 100 respond_to do |format|
101 101 format.html { redirect_to_settings_in_projects }
102 102 format.js
103 103 format.api {
104 104 if @member.destroyed?
105 105 render_api_ok
106 106 else
107 107 head :unprocessable_entity
108 108 end
109 109 }
110 110 end
111 111 end
112 112
113 113 def autocomplete
114 114 respond_to do |format|
115 115 format.js
116 116 end
117 117 end
118 118
119 119 private
120 120
121 121 def redirect_to_settings_in_projects
122 122 redirect_to settings_project_path(@project, :tab => 'members')
123 123 end
124 124 end
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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").
47 47 limit(@reply_pages.per_page).
48 48 offset(@reply_pages.offset).
49 49 all
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>((.|\s)*?)</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,201 +1,201
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 helper :issues
24 24 helper :users
25 25 helper :custom_fields
26 26
27 27 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
28 28 'issuesreportedbyme' => :label_reported_issues,
29 29 'issueswatched' => :label_watched_issues,
30 30 'news' => :label_news_latest,
31 31 'calendar' => :label_calendar,
32 32 'documents' => :label_document_plural,
33 33 'timelog' => :label_spent_time
34 34 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
35 35
36 36 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
37 37 'right' => ['issuesreportedbyme']
38 38 }.freeze
39 39
40 40 def index
41 41 page
42 42 render :action => 'page'
43 43 end
44 44
45 45 # Show user's page
46 46 def page
47 47 @user = User.current
48 48 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
49 49 end
50 50
51 51 # Edit user's account
52 52 def account
53 53 @user = User.current
54 54 @pref = @user.pref
55 55 if request.post?
56 56 @user.safe_attributes = params[:user]
57 57 @user.pref.attributes = params[:pref]
58 58 if @user.save
59 59 @user.pref.save
60 60 set_language_if_valid @user.language
61 61 flash[:notice] = l(:notice_account_updated)
62 62 redirect_to my_account_path
63 63 return
64 64 end
65 65 end
66 66 end
67 67
68 68 # Destroys user's account
69 69 def destroy
70 70 @user = User.current
71 71 unless @user.own_account_deletable?
72 72 redirect_to my_account_path
73 73 return
74 74 end
75 75
76 76 if request.post? && params[:confirm]
77 77 @user.destroy
78 78 if @user.destroyed?
79 79 logout_user
80 80 flash[:notice] = l(:notice_account_deleted)
81 81 end
82 82 redirect_to home_path
83 83 end
84 84 end
85 85
86 86 # Manage user's password
87 87 def password
88 88 @user = User.current
89 89 unless @user.change_password_allowed?
90 90 flash[:error] = l(:notice_can_t_change_password)
91 91 redirect_to my_account_path
92 92 return
93 93 end
94 94 if request.post?
95 95 if !@user.check_password?(params[:password])
96 96 flash.now[:error] = l(:notice_account_wrong_password)
97 97 elsif params[:password] == params[:new_password]
98 98 flash.now[:error] = l(:notice_new_password_must_be_different)
99 99 else
100 100 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
101 101 @user.must_change_passwd = false
102 102 if @user.save
103 103 flash[:notice] = l(:notice_account_password_updated)
104 104 redirect_to my_account_path
105 105 end
106 106 end
107 107 end
108 108 end
109 109
110 110 # Create a new feeds key
111 111 def reset_rss_key
112 112 if request.post?
113 113 if User.current.rss_token
114 114 User.current.rss_token.destroy
115 115 User.current.reload
116 116 end
117 117 User.current.rss_key
118 118 flash[:notice] = l(:notice_feeds_access_key_reseted)
119 119 end
120 120 redirect_to my_account_path
121 121 end
122 122
123 123 # Create a new API key
124 124 def reset_api_key
125 125 if request.post?
126 126 if User.current.api_token
127 127 User.current.api_token.destroy
128 128 User.current.reload
129 129 end
130 130 User.current.api_key
131 131 flash[:notice] = l(:notice_api_access_key_reseted)
132 132 end
133 133 redirect_to my_account_path
134 134 end
135 135
136 136 # User's page layout configuration
137 137 def page_layout
138 138 @user = User.current
139 139 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
140 140 @block_options = []
141 141 BLOCKS.each do |k, v|
142 142 unless @blocks.values.flatten.include?(k)
143 143 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
144 144 end
145 145 end
146 146 end
147 147
148 148 # Add a block to user's page
149 149 # The block is added on top of the page
150 150 # params[:block] : id of the block to add
151 151 def add_block
152 152 block = params[:block].to_s.underscore
153 153 if block.present? && BLOCKS.key?(block)
154 154 @user = User.current
155 155 layout = @user.pref[:my_page_layout] || {}
156 156 # remove if already present in a group
157 157 %w(top left right).each {|f| (layout[f] ||= []).delete block }
158 158 # add it on top
159 159 layout['top'].unshift block
160 160 @user.pref[:my_page_layout] = layout
161 161 @user.pref.save
162 162 end
163 163 redirect_to my_page_layout_path
164 164 end
165 165
166 166 # Remove a block to user's page
167 167 # params[:block] : id of the block to remove
168 168 def remove_block
169 169 block = params[:block].to_s.underscore
170 170 @user = User.current
171 171 # remove block in all groups
172 172 layout = @user.pref[:my_page_layout] || {}
173 173 %w(top left right).each {|f| (layout[f] ||= []).delete block }
174 174 @user.pref[:my_page_layout] = layout
175 175 @user.pref.save
176 176 redirect_to my_page_layout_path
177 177 end
178 178
179 179 # Change blocks order on user's page
180 180 # params[:group] : group to order (top, left or right)
181 181 # params[:list-(top|left|right)] : array of block ids of the group
182 182 def order_blocks
183 183 group = params[:group]
184 184 @user = User.current
185 185 if group.is_a?(String)
186 186 group_items = (params["blocks"] || []).collect(&:underscore)
187 187 group_items.each {|s| s.sub!(/^block_/, '')}
188 188 if group_items and group_items.is_a? Array
189 189 layout = @user.pref[:my_page_layout] || {}
190 190 # remove group blocks if they are presents in other groups
191 191 %w(top left right).each {|f|
192 192 layout[f] = (layout[f] || []) - group_items
193 193 }
194 194 layout[group] = group_items
195 195 @user.pref[:my_page_layout] = layout
196 196 @user.pref.save
197 197 end
198 198 end
199 199 render :nothing => true
200 200 end
201 201 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 all
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
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-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 = @project.issues.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,42 +1,42
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 request.put? && params[:enumerations]
24 24 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 flash[:notice] = l(:notice_successful_update)
30 30 end
31 31
32 32 redirect_to settings_project_path(@project, :tab => 'activities')
33 33 end
34 34
35 35 def destroy
36 36 @project.time_entry_activities.each do |time_entry_activity|
37 37 time_entry_activity.destroy(time_entry_activity.parent)
38 38 end
39 39 flash[:notice] = l(:notice_successful_update)
40 40 redirect_to settings_project_path(@project, :tab => 'activities')
41 41 end
42 42 end
@@ -1,262 +1,262
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :roadmap, :only => :roadmap
21 21 menu_item :settings, :only => :settings
22 22
23 23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 25 before_filter :authorize_global, :only => [:new, :create]
26 26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 27 accept_rss_auth :index
28 28 accept_api_auth :index, :show, :create, :update, :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 :sort
37 37 include SortHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issues
41 41 helper :queries
42 42 include QueriesHelper
43 43 helper :repositories
44 44 include RepositoriesHelper
45 45 include ProjectsHelper
46 46 helper :members
47 47
48 48 # Lists visible projects
49 49 def index
50 50 respond_to do |format|
51 51 format.html {
52 52 scope = Project
53 53 unless params[:closed]
54 54 scope = scope.active
55 55 end
56 56 @projects = scope.visible.order('lft').all
57 57 }
58 58 format.api {
59 59 @offset, @limit = api_offset_and_limit
60 60 @project_count = Project.visible.count
61 61 @projects = Project.visible.offset(@offset).limit(@limit).order('lft').all
62 62 }
63 63 format.atom {
64 64 projects = Project.visible.order('created_on DESC').limit(Setting.feeds_limit.to_i).all
65 65 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
66 66 }
67 67 end
68 68 end
69 69
70 70 def new
71 71 @issue_custom_fields = IssueCustomField.sorted.all
72 72 @trackers = Tracker.sorted.all
73 73 @project = Project.new
74 74 @project.safe_attributes = params[:project]
75 75 end
76 76
77 77 def create
78 78 @issue_custom_fields = IssueCustomField.sorted.all
79 79 @trackers = Tracker.sorted.all
80 80 @project = Project.new
81 81 @project.safe_attributes = params[:project]
82 82
83 83 if validate_parent_id && @project.save
84 84 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
85 85 # Add current user as a project member if current user is not admin
86 86 unless User.current.admin?
87 87 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
88 88 m = Member.new(:user => User.current, :roles => [r])
89 89 @project.members << m
90 90 end
91 91 respond_to do |format|
92 92 format.html {
93 93 flash[:notice] = l(:notice_successful_create)
94 94 if params[:continue]
95 95 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
96 96 redirect_to new_project_path(attrs)
97 97 else
98 98 redirect_to settings_project_path(@project)
99 99 end
100 100 }
101 101 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
102 102 end
103 103 else
104 104 respond_to do |format|
105 105 format.html { render :action => 'new' }
106 106 format.api { render_validation_errors(@project) }
107 107 end
108 108 end
109 109 end
110 110
111 111 def copy
112 112 @issue_custom_fields = IssueCustomField.sorted.all
113 113 @trackers = Tracker.sorted.all
114 114 @source_project = Project.find(params[:id])
115 115 if request.get?
116 116 @project = Project.copy_from(@source_project)
117 117 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
118 118 else
119 119 Mailer.with_deliveries(params[:notifications] == '1') do
120 120 @project = Project.new
121 121 @project.safe_attributes = params[:project]
122 122 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
123 123 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
124 124 flash[:notice] = l(:notice_successful_create)
125 125 redirect_to settings_project_path(@project)
126 126 elsif !@project.new_record?
127 127 # Project was created
128 128 # But some objects were not copied due to validation failures
129 129 # (eg. issues from disabled trackers)
130 130 # TODO: inform about that
131 131 redirect_to settings_project_path(@project)
132 132 end
133 133 end
134 134 end
135 135 rescue ActiveRecord::RecordNotFound
136 136 # source_project not found
137 137 render_404
138 138 end
139 139
140 140 # Show @project
141 141 def show
142 142 # try to redirect to the requested menu item
143 143 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
144 144 return
145 145 end
146 146
147 147 @users_by_role = @project.users_by_role
148 148 @subprojects = @project.children.visible.all
149 149 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").all
150 150 @trackers = @project.rolled_up_trackers
151 151
152 152 cond = @project.project_condition(Setting.display_subprojects_issues?)
153 153
154 154 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
155 155 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
156 156
157 157 if User.current.allowed_to?(:view_time_entries, @project)
158 158 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
159 159 end
160 160
161 161 @key = User.current.rss_key
162 162
163 163 respond_to do |format|
164 164 format.html
165 165 format.api
166 166 end
167 167 end
168 168
169 169 def settings
170 170 @issue_custom_fields = IssueCustomField.sorted.all
171 171 @issue_category ||= IssueCategory.new
172 172 @member ||= @project.members.new
173 173 @trackers = Tracker.sorted.all
174 174 @wiki ||= @project.wiki
175 175 end
176 176
177 177 def edit
178 178 end
179 179
180 180 def update
181 181 @project.safe_attributes = params[:project]
182 182 if validate_parent_id && @project.save
183 183 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
184 184 respond_to do |format|
185 185 format.html {
186 186 flash[:notice] = l(:notice_successful_update)
187 187 redirect_to settings_project_path(@project)
188 188 }
189 189 format.api { render_api_ok }
190 190 end
191 191 else
192 192 respond_to do |format|
193 193 format.html {
194 194 settings
195 195 render :action => 'settings'
196 196 }
197 197 format.api { render_validation_errors(@project) }
198 198 end
199 199 end
200 200 end
201 201
202 202 def modules
203 203 @project.enabled_module_names = params[:enabled_module_names]
204 204 flash[:notice] = l(:notice_successful_update)
205 205 redirect_to settings_project_path(@project, :tab => 'modules')
206 206 end
207 207
208 208 def archive
209 209 if request.post?
210 210 unless @project.archive
211 211 flash[:error] = l(:error_can_not_archive_project)
212 212 end
213 213 end
214 214 redirect_to admin_projects_path(:status => params[:status])
215 215 end
216 216
217 217 def unarchive
218 218 @project.unarchive if request.post? && !@project.active?
219 219 redirect_to admin_projects_path(:status => params[:status])
220 220 end
221 221
222 222 def close
223 223 @project.close
224 224 redirect_to project_path(@project)
225 225 end
226 226
227 227 def reopen
228 228 @project.reopen
229 229 redirect_to project_path(@project)
230 230 end
231 231
232 232 # Delete @project
233 233 def destroy
234 234 @project_to_destroy = @project
235 235 if api_request? || params[:confirm]
236 236 @project_to_destroy.destroy
237 237 respond_to do |format|
238 238 format.html { redirect_to admin_projects_path }
239 239 format.api { render_api_ok }
240 240 end
241 241 end
242 242 # hide project in layout
243 243 @project = nil
244 244 end
245 245
246 246 private
247 247
248 248 # Validates parent_id param according to user's permissions
249 249 # TODO: move it to Project model in a validation that depends on User.current
250 250 def validate_parent_id
251 251 return true if User.current.admin?
252 252 parent_id = params[:project] && params[:project][:parent_id]
253 253 if parent_id || @project.new_record?
254 254 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
255 255 unless @project.allowed_parents.include?(parent)
256 256 @project.errors.add :parent_id, :invalid
257 257 return false
258 258 end
259 259 end
260 260 true
261 261 end
262 262 end
@@ -1,120 +1,120
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 all
41 41 respond_to do |format|
42 42 format.api
43 43 end
44 44 end
45 45
46 46 def new
47 47 @query = IssueQuery.new
48 48 @query.user = User.current
49 49 @query.project = @project
50 50 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
51 51 @query.build_from_params(params)
52 52 end
53 53
54 54 def create
55 55 @query = IssueQuery.new(params[:query])
56 56 @query.user = User.current
57 57 @query.project = params[:query_is_for_all] ? nil : @project
58 58 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
59 59 @query.build_from_params(params)
60 60 @query.column_names = nil if params[:default_columns]
61 61
62 62 if @query.save
63 63 flash[:notice] = l(:notice_successful_create)
64 64 redirect_to_issues(:query_id => @query)
65 65 else
66 66 render :action => 'new', :layout => !request.xhr?
67 67 end
68 68 end
69 69
70 70 def edit
71 71 end
72 72
73 73 def update
74 74 @query.attributes = params[:query]
75 75 @query.project = nil if params[:query_is_for_all]
76 76 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
77 77 @query.build_from_params(params)
78 78 @query.column_names = nil if params[:default_columns]
79 79
80 80 if @query.save
81 81 flash[:notice] = l(:notice_successful_update)
82 82 redirect_to_issues(:query_id => @query)
83 83 else
84 84 render :action => 'edit'
85 85 end
86 86 end
87 87
88 88 def destroy
89 89 @query.destroy
90 90 redirect_to_issues(:set_filter => 1)
91 91 end
92 92
93 93 private
94 94 def find_query
95 95 @query = IssueQuery.find(params[:id])
96 96 @project = @query.project
97 97 render_403 unless @query.editable_by?(User.current)
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101
102 102 def find_optional_project
103 103 @project = Project.find(params[:project_id]) if params[:project_id]
104 104 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
105 105 rescue ActiveRecord::RecordNotFound
106 106 render_404
107 107 end
108 108
109 109 def redirect_to_issues(options)
110 110 if params[:gantt]
111 111 if @project
112 112 redirect_to project_gantt_path(@project, options)
113 113 else
114 114 redirect_to issues_gantt_path(options)
115 115 end
116 116 else
117 117 redirect_to _project_issues_path(@project, options)
118 118 end
119 119 end
120 120 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.all
94 94 end
95 95 end
@@ -1,438 +1,438
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 request.put? && @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
96 96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 97 @users += User.where(:id => additional_user_ids).all 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 all
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 commits_by_author = Changeset.where("repository_id = ?", repository.id).group(:committer).count
401 401 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
402 402
403 403 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", repository.id).group(:committer).count
404 404 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
405 405
406 406 fields = commits_by_author.collect {|r| r.first}
407 407 commits_data = commits_by_author.collect {|r| r.last}
408 408 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
409 409
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 adress in usernames
415 415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
416 416
417 417 graph = SVG::Graph::BarHorizontal.new(
418 418 :height => 30 * commits_data.length,
419 419 :width => 800,
420 420 :fields => fields,
421 421 :stack => :side,
422 422 :scale_integers => true,
423 423 :show_data_values => false,
424 424 :rotate_y_labels => false,
425 425 :graph_title => l(:label_commits_per_author),
426 426 :show_graph_title => true
427 427 )
428 428 graph.add_data(
429 429 :data => commits_data,
430 430 :title => l(:label_revision_plural)
431 431 )
432 432 graph.add_data(
433 433 :data => changes_data,
434 434 :title => l(:label_change_plural)
435 435 )
436 436 graph.burn
437 437 end
438 438 end
@@ -1,108 +1,108
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 def index
27 27 respond_to do |format|
28 28 format.html {
29 29 @role_pages, @roles = paginate Role.sorted, :per_page => 25
30 30 render :action => "index", :layout => false if request.xhr?
31 31 }
32 32 format.api {
33 33 @roles = Role.givable.all
34 34 }
35 35 end
36 36 end
37 37
38 38 def show
39 39 respond_to do |format|
40 40 format.api
41 41 end
42 42 end
43 43
44 44 def new
45 45 # Prefills the form with 'Non member' role permissions by default
46 46 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
47 47 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
48 48 @role.copy_from(@copy_from)
49 49 end
50 50 @roles = Role.sorted.all
51 51 end
52 52
53 53 def create
54 54 @role = Role.new(params[:role])
55 55 if request.post? && @role.save
56 56 # workflow copy
57 57 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
58 58 @role.workflow_rules.copy(copy_from)
59 59 end
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to roles_path
62 62 else
63 63 @roles = Role.sorted.all
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 if request.put? and @role.update_attributes(params[:role])
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to roles_path
75 75 else
76 76 render :action => 'edit'
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @role.destroy
82 82 redirect_to roles_path
83 83 rescue
84 84 flash[:error] = l(:error_can_not_remove_role)
85 85 redirect_to roles_path
86 86 end
87 87
88 88 def permissions
89 89 @roles = Role.sorted.all
90 90 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
91 91 if request.post?
92 92 @roles.each do |role|
93 93 role.permissions = params[:permissions][role.id.to_s]
94 94 role.save
95 95 end
96 96 flash[:notice] = l(:notice_successful_update)
97 97 redirect_to roles_path
98 98 end
99 99 end
100 100
101 101 private
102 102
103 103 def find_role
104 104 @role = Role.find(params[:id])
105 105 rescue ActiveRecord::RecordNotFound
106 106 render_404
107 107 end
108 108 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
27 27 projects_to_search =
28 28 case params[:scope]
29 29 when 'all'
30 30 nil
31 31 when 'my_projects'
32 32 User.current.memberships.collect(&:project)
33 33 when 'subprojects'
34 34 @project ? (@project.self_and_descendants.active.all) : nil
35 35 else
36 36 @project
37 37 end
38 38
39 39 offset = nil
40 40 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
41 41
42 42 # quick jump to an issue
43 43 if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
44 44 redirect_to issue_path(issue)
45 45 return
46 46 end
47 47
48 48 @object_types = Redmine::Search.available_search_types.dup
49 49 if projects_to_search.is_a? Project
50 50 # don't search projects
51 51 @object_types.delete('projects')
52 52 # only show what the user is allowed to view
53 53 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
54 54 end
55 55
56 56 @scope = @object_types.select {|t| params[t]}
57 57 @scope = @object_types if @scope.empty?
58 58
59 59 # extract tokens from the question
60 60 # eg. hello "bye bye" => ["hello", "bye bye"]
61 61 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
62 62 # tokens must be at least 2 characters long
63 63 @tokens = @tokens.uniq.select {|w| w.length > 1 }
64 64
65 65 if !@tokens.empty?
66 66 # no more than 5 tokens to search for
67 67 @tokens.slice! 5..-1 if @tokens.size > 5
68 68
69 69 @results = []
70 70 @results_by_type = Hash.new {|h,k| h[k] = 0}
71 71
72 72 limit = 10
73 73 @scope.each do |s|
74 74 r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
75 75 :all_words => @all_words,
76 76 :titles_only => @titles_only,
77 77 :limit => (limit+1),
78 78 :offset => offset,
79 79 :before => params[:previous].nil?)
80 80 @results += r
81 81 @results_by_type[s] += c
82 82 end
83 83 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
84 84 if params[:previous].nil?
85 85 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
86 86 if @results.size > limit
87 87 @pagination_next_date = @results[limit-1].event_datetime
88 88 @results = @results[0, limit]
89 89 end
90 90 else
91 91 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
92 92 if @results.size > limit
93 93 @pagination_previous_date = @results[-(limit)].event_datetime
94 94 @results = @results[-(limit), limit]
95 95 end
96 96 end
97 97 else
98 98 @question = ""
99 99 end
100 100 render :layout => false if request.xhr?
101 101 end
102 102
103 103 private
104 104 def find_optional_project
105 105 return true unless params[:id]
106 106 @project = Project.find(params[:id])
107 107 check_project_privacy
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,74 +1,74
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 def index
27 27 edit
28 28 render :action => 'edit'
29 29 end
30 30
31 31 def edit
32 32 @notifiables = Redmine::Notifiable.all
33 33 if request.post? && params[:settings] && params[:settings].is_a?(Hash)
34 34 settings = (params[:settings] || {}).dup.symbolize_keys
35 35 settings.each do |name, value|
36 36 Setting.set_from_params name, value
37 37 end
38 38 flash[:notice] = l(:notice_successful_update)
39 39 redirect_to settings_path(:tab => params[:tab])
40 40 else
41 41 @options = {}
42 42 user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
43 43 @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
44 44 @deliveries = ActionMailer::Base.perform_deliveries
45 45
46 46 @guessed_host_and_path = request.host_with_port.dup
47 47 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
48 48
49 49 @commit_update_keywords = Setting.commit_update_keywords.dup
50 50 @commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?
51 51
52 52 Redmine::Themes.rescan
53 53 end
54 54 end
55 55
56 56 def plugin
57 57 @plugin = Redmine::Plugin.find(params[:id])
58 58 unless @plugin.configurable?
59 59 render_404
60 60 return
61 61 end
62 62
63 63 if request.post?
64 64 Setting.send "plugin_#{@plugin.id}=", params[:settings]
65 65 flash[:notice] = l(:notice_successful_update)
66 66 redirect_to plugin_settings_path(@plugin)
67 67 else
68 68 @partial = @plugin.settings[:partial]
69 69 @settings = Setting.send "plugin_#{@plugin.id}"
70 70 end
71 71 rescue Redmine::PluginNotFound
72 72 render_404
73 73 end
74 74 end
@@ -1,80 +1,80
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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).order("#{Project.table_name}.identifier").preload(:repository).all
23 23 # extra_info attribute from repository breaks activeresource client
24 24 render :xml => p.to_xml(
25 25 :only => [:id, :identifier, :name, :is_public, :status],
26 26 :include => {:repository => {:only => [:id, :url]}}
27 27 )
28 28 end
29 29
30 30 def create_project_repository
31 31 project = Project.find(params[:id])
32 32 if project.repository
33 33 render :nothing => true, :status => 409
34 34 else
35 35 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
36 36 repository = Repository.factory(params[:vendor], params[:repository])
37 37 repository.project = project
38 38 if repository.save
39 39 render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
40 40 else
41 41 render :nothing => true, :status => 422
42 42 end
43 43 end
44 44 end
45 45
46 46 def fetch_changesets
47 47 projects = []
48 48 scope = Project.active.has_module(:repository)
49 49 if params[:id]
50 50 project = nil
51 51 if params[:id].to_s =~ /^\d*$/
52 52 project = scope.find(params[:id])
53 53 else
54 54 project = scope.find_by_identifier(params[:id])
55 55 end
56 56 raise ActiveRecord::RecordNotFound unless project
57 57 projects << project
58 58 else
59 59 projects = scope.all
60 60 end
61 61 projects.each do |project|
62 62 project.repositories.each do |repository|
63 63 repository.fetch_changesets
64 64 end
65 65 end
66 66 render :nothing => true, :status => 200
67 67 rescue ActiveRecord::RecordNotFound
68 68 render :nothing => true, :status => 404
69 69 end
70 70
71 71 protected
72 72
73 73 def check_enabled
74 74 User.current = nil
75 75 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
76 76 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
77 77 return false
78 78 end
79 79 end
80 80 end
@@ -1,297 +1,297
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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_project_for_new_time_entry, :only => [:create]
22 22 before_filter :find_time_entry, :only => [:show, :edit, :update]
23 23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 24 before_filter :authorize, :except => [:new, :index, :report]
25 25
26 26 before_filter :find_optional_project, :only => [:index, :report]
27 27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
28 28 before_filter :authorize_global, :only => [:new, :index, :report]
29 29
30 30 accept_rss_auth :index
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :sort
36 36 include SortHelper
37 37 helper :issues
38 38 include TimelogHelper
39 39 helper :custom_fields
40 40 include CustomFieldsHelper
41 41 helper :queries
42 42 include QueriesHelper
43 43
44 44 def index
45 45 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
46 46
47 47 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
48 48 sort_update(@query.sortable_columns)
49 49 scope = time_entry_scope(:order => sort_clause).
50 50 includes(:project, :user, :issue).
51 51 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
52 52
53 53 respond_to do |format|
54 54 format.html {
55 55 @entry_count = scope.count
56 56 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
57 57 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).all
58 58 @total_hours = scope.sum(:hours).to_f
59 59
60 60 render :layout => !request.xhr?
61 61 }
62 62 format.api {
63 63 @entry_count = scope.count
64 64 @offset, @limit = api_offset_and_limit
65 65 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).all
66 66 }
67 67 format.atom {
68 68 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").all
69 69 render_feed(entries, :title => l(:label_spent_time))
70 70 }
71 71 format.csv {
72 72 # Export all entries
73 73 @entries = scope.all
74 74 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
75 75 }
76 76 end
77 77 end
78 78
79 79 def report
80 80 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
81 81 scope = time_entry_scope
82 82
83 83 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
84 84
85 85 respond_to do |format|
86 86 format.html { render :layout => !request.xhr? }
87 87 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
88 88 end
89 89 end
90 90
91 91 def show
92 92 respond_to do |format|
93 93 # TODO: Implement html response
94 94 format.html { render :nothing => true, :status => 406 }
95 95 format.api
96 96 end
97 97 end
98 98
99 99 def new
100 100 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
101 101 @time_entry.safe_attributes = params[:time_entry]
102 102 end
103 103
104 104 def create
105 105 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
106 106 @time_entry.safe_attributes = params[:time_entry]
107 107
108 108 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
109 109
110 110 if @time_entry.save
111 111 respond_to do |format|
112 112 format.html {
113 113 flash[:notice] = l(:notice_successful_create)
114 114 if params[:continue]
115 115 if params[:project_id]
116 116 options = {
117 117 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
118 118 :back_url => params[:back_url]
119 119 }
120 120 if @time_entry.issue
121 121 redirect_to new_project_issue_time_entry_path(@time_entry.project, @time_entry.issue, options)
122 122 else
123 123 redirect_to new_project_time_entry_path(@time_entry.project, options)
124 124 end
125 125 else
126 126 options = {
127 127 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
128 128 :back_url => params[:back_url]
129 129 }
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]).all
236 236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 237 @projects = @time_entries.collect(&:project).compact.uniq
238 238 @project = @projects.first if @projects.size == 1
239 239 rescue ActiveRecord::RecordNotFound
240 240 render_404
241 241 end
242 242
243 243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 244 if unsaved_time_entry_ids.empty?
245 245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 246 else
247 247 flash[:error] = l(:notice_failed_to_save_time_entries,
248 248 :count => unsaved_time_entry_ids.size,
249 249 :total => time_entries.size,
250 250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 251 end
252 252 end
253 253
254 254 def find_optional_project_for_new_time_entry
255 255 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
256 256 @project = Project.find(project_id)
257 257 end
258 258 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
259 259 @issue = Issue.find(issue_id)
260 260 @project ||= @issue.project
261 261 end
262 262 rescue ActiveRecord::RecordNotFound
263 263 render_404
264 264 end
265 265
266 266 def find_project_for_new_time_entry
267 267 find_optional_project_for_new_time_entry
268 268 if @project.nil?
269 269 render_404
270 270 end
271 271 end
272 272
273 273 def find_optional_project
274 274 if !params[:issue_id].blank?
275 275 @issue = Issue.find(params[:issue_id])
276 276 @project = @issue.project
277 277 elsif !params[:project_id].blank?
278 278 @project = Project.find(params[:project_id])
279 279 end
280 280 end
281 281
282 282 # Returns the TimeEntry scope for index and report actions
283 283 def time_entry_scope(options={})
284 284 scope = @query.results_scope(options)
285 285 if @issue
286 286 scope = scope.on_issue(@issue)
287 287 end
288 288 scope
289 289 end
290 290
291 291 def parse_params_for_bulk_time_entry_attributes(params)
292 292 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
293 293 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
294 294 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
295 295 attributes
296 296 end
297 297 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.all
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.all
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
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.all
99 99 @custom_fields = IssueCustomField.all.sort
100 100 end
101 101 end
@@ -1,208 +1,208
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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, :edit_membership, :destroy_membership]
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
30 30 def index
31 31 sort_init 'login', 'asc'
32 32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33 33
34 34 case params[:format]
35 35 when 'xml', 'json'
36 36 @offset, @limit = api_offset_and_limit
37 37 else
38 38 @limit = per_page_option
39 39 end
40 40
41 41 @status = params[:status] || 1
42 42
43 43 scope = User.logged.status(@status)
44 44 scope = scope.like(params[:name]) if params[:name].present?
45 45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
46 46
47 47 @user_count = scope.count
48 48 @user_pages = Paginator.new @user_count, @limit, params['page']
49 49 @offset ||= @user_pages.offset
50 50 @users = scope.order(sort_clause).limit(@limit).offset(@offset).all
51 51
52 52 respond_to do |format|
53 53 format.html {
54 54 @groups = Group.all.sort
55 55 render :layout => !request.xhr?
56 56 }
57 57 format.api
58 58 end
59 59 end
60 60
61 61 def show
62 62 # show projects based on current user visibility
63 63 @memberships = @user.memberships.where(Project.visible_condition(User.current)).all
64 64
65 65 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
66 66 @events_by_day = events.group_by(&:event_date)
67 67
68 68 unless User.current.admin?
69 69 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
70 70 render_404
71 71 return
72 72 end
73 73 end
74 74
75 75 respond_to do |format|
76 76 format.html { render :layout => 'base' }
77 77 format.api
78 78 end
79 79 end
80 80
81 81 def new
82 82 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
83 83 @user.safe_attributes = params[:user]
84 84 @auth_sources = AuthSource.all
85 85 end
86 86
87 87 def create
88 88 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
89 89 @user.safe_attributes = params[:user]
90 90 @user.admin = params[:user][:admin] || false
91 91 @user.login = params[:user][:login]
92 92 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
93 93 @user.pref.attributes = params[:pref]
94 94
95 95 if @user.save
96 96 Mailer.account_information(@user, @user.password).deliver if params[:send_information]
97 97
98 98 respond_to do |format|
99 99 format.html {
100 100 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
101 101 if params[:continue]
102 102 attrs = params[:user].slice(:generate_password)
103 103 redirect_to new_user_path(:user => attrs)
104 104 else
105 105 redirect_to edit_user_path(@user)
106 106 end
107 107 }
108 108 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
109 109 end
110 110 else
111 111 @auth_sources = AuthSource.all
112 112 # Clear password input
113 113 @user.password = @user.password_confirmation = nil
114 114
115 115 respond_to do |format|
116 116 format.html { render :action => 'new' }
117 117 format.api { render_validation_errors(@user) }
118 118 end
119 119 end
120 120 end
121 121
122 122 def edit
123 123 @auth_sources = AuthSource.all
124 124 @membership ||= Member.new
125 125 end
126 126
127 127 def update
128 128 @user.admin = params[:user][:admin] if params[:user][:admin]
129 129 @user.login = params[:user][:login] if params[:user][:login]
130 130 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
131 131 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
132 132 end
133 133 @user.safe_attributes = params[:user]
134 134 # Was the account actived ? (do it before User#save clears the change)
135 135 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
136 136 # TODO: Similar to My#account
137 137 @user.pref.attributes = params[:pref]
138 138
139 139 if @user.save
140 140 @user.pref.save
141 141
142 142 if was_activated
143 143 Mailer.account_activated(@user).deliver
144 144 elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil?
145 145 Mailer.account_information(@user, @user.password).deliver
146 146 end
147 147
148 148 respond_to do |format|
149 149 format.html {
150 150 flash[:notice] = l(:notice_successful_update)
151 151 redirect_to_referer_or edit_user_path(@user)
152 152 }
153 153 format.api { render_api_ok }
154 154 end
155 155 else
156 156 @auth_sources = AuthSource.all
157 157 @membership ||= Member.new
158 158 # Clear password input
159 159 @user.password = @user.password_confirmation = nil
160 160
161 161 respond_to do |format|
162 162 format.html { render :action => :edit }
163 163 format.api { render_validation_errors(@user) }
164 164 end
165 165 end
166 166 end
167 167
168 168 def destroy
169 169 @user.destroy
170 170 respond_to do |format|
171 171 format.html { redirect_back_or_default(users_path) }
172 172 format.api { render_api_ok }
173 173 end
174 174 end
175 175
176 176 def edit_membership
177 177 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
178 178 @membership.save
179 179 respond_to do |format|
180 180 format.html { redirect_to edit_user_path(@user, :tab => 'memberships') }
181 181 format.js
182 182 end
183 183 end
184 184
185 185 def destroy_membership
186 186 @membership = Member.find(params[:membership_id])
187 187 if @membership.deletable?
188 188 @membership.destroy
189 189 end
190 190 respond_to do |format|
191 191 format.html { redirect_to edit_user_path(@user, :tab => 'memberships') }
192 192 format.js
193 193 end
194 194 end
195 195
196 196 private
197 197
198 198 def find_user
199 199 if params[:id] == 'current'
200 200 require_login || return
201 201 @user = User.current
202 202 else
203 203 @user = User.find(params[:id])
204 204 end
205 205 rescue ActiveRecord::RecordNotFound
206 206 render_404
207 207 end
208 208 end
@@ -1,182 +1,182
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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.all
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.all
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 all
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 request.put? && 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.fixed_issues.empty?
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,120 +1,120
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 user_ids.flatten.compact.uniq.each do |user_id|
44 44 Watcher.create(:watchable => @watched, :user_id => user_id)
45 45 end
46 46 respond_to do |format|
47 47 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
48 48 format.js { @users = users_for_new_watcher }
49 49 format.api { render_api_ok }
50 50 end
51 51 end
52 52
53 53 def append
54 54 if params[:watcher].is_a?(Hash)
55 55 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
56 56 @users = User.active.where(:id => user_ids).all
57 57 end
58 58 end
59 59
60 60 def destroy
61 61 @watched.set_watcher(User.find(params[:user_id]), false)
62 62 respond_to do |format|
63 63 format.html { redirect_to :back }
64 64 format.js
65 65 format.api { render_api_ok }
66 66 end
67 67 end
68 68
69 69 def autocomplete_for_user
70 70 @users = users_for_new_watcher
71 71 render :layout => false
72 72 end
73 73
74 74 private
75 75
76 76 def find_project
77 77 if params[:object_type] && params[:object_id]
78 78 klass = Object.const_get(params[:object_type].camelcase)
79 79 return false unless klass.respond_to?('watched_by')
80 80 @watched = klass.find(params[:object_id])
81 81 @project = @watched.project
82 82 elsif params[:project_id]
83 83 @project = Project.visible.find_by_param(params[:project_id])
84 84 end
85 85 rescue
86 86 render_404
87 87 end
88 88
89 89 def find_watchables
90 90 klass = Object.const_get(params[:object_type].camelcase) rescue nil
91 91 if klass && klass.respond_to?('watched_by')
92 92 @watchables = klass.where(:id => Array.wrap(params[:object_id])).all
93 93 raise Unauthorized if @watchables.any? {|w| w.respond_to?(:visible?) && !w.visible?}
94 94 end
95 95 render_404 unless @watchables.present?
96 96 end
97 97
98 98 def set_watcher(watchables, user, watching)
99 99 watchables.each do |watchable|
100 100 watchable.set_watcher(user, watching)
101 101 end
102 102 respond_to do |format|
103 103 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
104 104 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
105 105 end
106 106 end
107 107
108 108 def users_for_new_watcher
109 109 users = []
110 110 if params[:q].blank? && @project.present?
111 111 users = @project.users.sorted
112 112 else
113 113 users = User.active.sorted.like(params[:q]).limit(100)
114 114 end
115 115 if @watched
116 116 users -= @watched.watcher_users
117 117 end
118 118 users
119 119 end
120 120 end
@@ -1,30 +1,30
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 @projects = Project.latest User.current
24 24 end
25 25
26 26 def robots
27 27 @projects = Project.all_public.active
28 28 render :layout => false, :content_type => 'text/plain'
29 29 end
30 30 end
@@ -1,365 +1,365
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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_data(wiki_page_to_pdf(@page, @project), :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 rescue ActiveRecord::RecordNotSaved
189 189 respond_to do |format|
190 190 format.html { render :action => 'edit' }
191 191 format.api { render_validation_errors(@content) }
192 192 end
193 193 end
194 194
195 195 # rename a page
196 196 def rename
197 197 return render_403 unless editable?
198 198 @page.redirect_existing_links = true
199 199 # used to display the *original* title if some AR validation errors occur
200 200 @original_title = @page.pretty_title
201 201 if request.post? && @page.update_attributes(params[:wiki_page])
202 202 flash[:notice] = l(:notice_successful_update)
203 203 redirect_to project_wiki_page_path(@project, @page.title)
204 204 end
205 205 end
206 206
207 207 def protect
208 208 @page.update_attribute :protected, params[:protected]
209 209 redirect_to project_wiki_page_path(@project, @page.title)
210 210 end
211 211
212 212 # show page history
213 213 def history
214 214 @version_count = @page.content.versions.count
215 215 @version_pages = Paginator.new @version_count, per_page_option, params['page']
216 216 # don't load text
217 217 @versions = @page.content.versions.
218 218 select("id, author_id, comments, updated_on, version").
219 219 reorder('version DESC').
220 220 limit(@version_pages.per_page + 1).
221 221 offset(@version_pages.offset).
222 222 all
223 223
224 224 render :layout => false if request.xhr?
225 225 end
226 226
227 227 def diff
228 228 @diff = @page.diff(params[:version], params[:version_from])
229 229 render_404 unless @diff
230 230 end
231 231
232 232 def annotate
233 233 @annotate = @page.annotate(params[:version])
234 234 render_404 unless @annotate
235 235 end
236 236
237 237 # Removes a wiki page and its history
238 238 # Children can be either set as root pages, removed or reassigned to another parent page
239 239 def destroy
240 240 return render_403 unless editable?
241 241
242 242 @descendants_count = @page.descendants.size
243 243 if @descendants_count > 0
244 244 case params[:todo]
245 245 when 'nullify'
246 246 # Nothing to do
247 247 when 'destroy'
248 248 # Removes all its descendants
249 249 @page.descendants.each(&:destroy)
250 250 when 'reassign'
251 251 # Reassign children to another parent page
252 252 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
253 253 return unless reassign_to
254 254 @page.children.each do |child|
255 255 child.update_attribute(:parent, reassign_to)
256 256 end
257 257 else
258 258 @reassignable_to = @wiki.pages - @page.self_and_descendants
259 259 # display the destroy form if it's a user request
260 260 return unless api_request?
261 261 end
262 262 end
263 263 @page.destroy
264 264 respond_to do |format|
265 265 format.html { redirect_to project_wiki_index_path(@project) }
266 266 format.api { render_api_ok }
267 267 end
268 268 end
269 269
270 270 def destroy_version
271 271 return render_403 unless editable?
272 272
273 273 @content = @page.content_for_version(params[:version])
274 274 @content.destroy
275 275 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
276 276 end
277 277
278 278 # Export wiki to a single pdf or html file
279 279 def export
280 280 @pages = @wiki.pages.
281 281 order('title').
282 282 includes([:content, {:attachments => :author}]).
283 283 all
284 284 respond_to do |format|
285 285 format.html {
286 286 export = render_to_string :action => 'export_multiple', :layout => false
287 287 send_data(export, :type => 'text/html', :filename => "wiki.html")
288 288 }
289 289 format.pdf {
290 290 send_data(wiki_pages_to_pdf(@pages, @project),
291 291 :type => 'application/pdf',
292 292 :filename => "#{@project.identifier}.pdf")
293 293 }
294 294 end
295 295 end
296 296
297 297 def preview
298 298 page = @wiki.find_page(params[:id])
299 299 # page is nil when previewing a new page
300 300 return render_403 unless page.nil? || editable?(page)
301 301 if page
302 302 @attachments += page.attachments
303 303 @previewed = page.content
304 304 end
305 305 @text = params[:content][:text]
306 306 render :partial => 'common/preview'
307 307 end
308 308
309 309 def add_attachment
310 310 return render_403 unless editable?
311 311 attachments = Attachment.attach_files(@page, params[:attachments])
312 312 render_attachment_warning_if_needed(@page)
313 313 redirect_to :action => 'show', :id => @page.title, :project_id => @project
314 314 end
315 315
316 316 private
317 317
318 318 def find_wiki
319 319 @project = Project.find(params[:project_id])
320 320 @wiki = @project.wiki
321 321 render_404 unless @wiki
322 322 rescue ActiveRecord::RecordNotFound
323 323 render_404
324 324 end
325 325
326 326 # Finds the requested page or a new page if it doesn't exist
327 327 def find_existing_or_new_page
328 328 @page = @wiki.find_or_new_page(params[:id])
329 329 if @wiki.page_found_with_redirect?
330 330 redirect_to params.update(:id => @page.title)
331 331 end
332 332 end
333 333
334 334 # Finds the requested page and returns a 404 error if it doesn't exist
335 335 def find_existing_page
336 336 @page = @wiki.find_page(params[:id])
337 337 if @page.nil?
338 338 render_404
339 339 return
340 340 end
341 341 if @wiki.page_found_with_redirect?
342 342 redirect_to params.update(:id => @page.title)
343 343 end
344 344 end
345 345
346 346 # Returns true if the current user is allowed to edit the page, otherwise false
347 347 def editable?(page = @page)
348 348 page.editable_by?(User.current)
349 349 end
350 350
351 351 # Returns the default content of a new wiki page
352 352 def initial_page_content(page)
353 353 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
354 354 extend helper unless self.instance_of?(helper)
355 355 helper.instance_method(:initial_page_content).bind(self).call(page)
356 356 end
357 357
358 358 def load_pages_for_index
359 359 @pages = @wiki.pages.with_updated_on.
360 360 reorder("#{WikiPage.table_name}.title").
361 361 includes(:wiki => :project).
362 362 includes(:parent).
363 363 all
364 364 end
365 365 end
@@ -1,36 +1,36
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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,129 +1,129
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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, :find_roles, :find_trackers
22 22
23 23 def index
24 24 @workflow_counts = WorkflowTransition.count_by_tracker_and_role
25 25 end
26 26
27 27 def edit
28 28 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
29 29 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
30 30
31 31 if request.post?
32 32 WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
33 33 (params[:issue_status] || []).each { |status_id, transitions|
34 34 transitions.each { |new_status_id, options|
35 35 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
36 36 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
37 37 WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 38 }
39 39 }
40 40 if @role.save
41 41 flash[:notice] = l(:notice_successful_update)
42 42 redirect_to workflows_edit_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
43 43 return
44 44 end
45 45 end
46 46
47 47 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
48 48 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
49 49 @statuses = @tracker.issue_statuses
50 50 end
51 51 @statuses ||= IssueStatus.sorted.all
52 52
53 53 if @tracker && @role && @statuses.any?
54 54 workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all
55 55 @workflows = {}
56 56 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
57 57 @workflows['author'] = workflows.select {|w| w.author}
58 58 @workflows['assignee'] = workflows.select {|w| w.assignee}
59 59 end
60 60 end
61 61
62 62 def permissions
63 63 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
64 64 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
65 65
66 66 if request.post? && @role && @tracker
67 67 WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {})
68 68 flash[:notice] = l(:notice_successful_update)
69 69 redirect_to workflows_permissions_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
70 70 return
71 71 end
72 72
73 73 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
74 74 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
75 75 @statuses = @tracker.issue_statuses
76 76 end
77 77 @statuses ||= IssueStatus.sorted.all
78 78
79 79 if @role && @tracker
80 80 @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
81 81 @custom_fields = @tracker.custom_fields
82 82 @permissions = WorkflowPermission.
83 83 where(:tracker_id => @tracker.id, :role_id => @role.id).inject({}) do |h, w|
84 84 h[w.old_status_id] ||= {}
85 85 h[w.old_status_id][w.field_name] = w.rule
86 86 h
87 87 end
88 88 @statuses.each {|status| @permissions[status.id] ||= {}}
89 89 end
90 90 end
91 91
92 92 def copy
93 93 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
94 94 @source_tracker = nil
95 95 else
96 96 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
97 97 end
98 98 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
99 99 @source_role = nil
100 100 else
101 101 @source_role = Role.find_by_id(params[:source_role_id].to_i)
102 102 end
103 103 @target_trackers = params[:target_tracker_ids].blank? ?
104 104 nil : Tracker.where(:id => params[:target_tracker_ids]).all
105 105 @target_roles = params[:target_role_ids].blank? ?
106 106 nil : Role.where(:id => params[:target_role_ids]).all
107 107 if request.post?
108 108 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
109 109 flash.now[:error] = l(:error_workflow_copy_source)
110 110 elsif @target_trackers.blank? || @target_roles.blank?
111 111 flash.now[:error] = l(:error_workflow_copy_target)
112 112 else
113 113 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
114 114 flash[:notice] = l(:notice_successful_update)
115 115 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
116 116 end
117 117 end
118 118 end
119 119
120 120 private
121 121
122 122 def find_roles
123 123 @roles = Role.sorted.all
124 124 end
125 125
126 126 def find_trackers
127 127 @trackers = Tracker.sorted.all
128 128 end
129 129 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,27 +1,27
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 end
@@ -1,1327 +1,1327
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = truncate(issue.subject, :length => 60)
76 76 else
77 77 subject = issue.subject
78 78 if options[:truncate]
79 79 subject = truncate(subject, :length => options[:truncate])
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to text, issue_path(issue, :only_path => only_path), :class => issue.css_classes, :title => title
84 84 s << h(": #{subject}") if subject
85 85 s = h("#{issue.project} - ") + s if options[:project]
86 86 s
87 87 end
88 88
89 89 # Generates a link to an attachment.
90 90 # Options:
91 91 # * :text - Link text (default to attachment filename)
92 92 # * :download - Force download (default: false)
93 93 def link_to_attachment(attachment, options={})
94 94 text = options.delete(:text) || attachment.filename
95 95 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 96 html_options = options.slice!(:only_path)
97 97 url = send(route_method, attachment, attachment.filename, options)
98 98 link_to text, url, html_options
99 99 end
100 100
101 101 # Generates a link to a SCM revision
102 102 # Options:
103 103 # * :text - Link text (default to the formatted revision)
104 104 def link_to_revision(revision, repository, options={})
105 105 if repository.is_a?(Project)
106 106 repository = repository.repository
107 107 end
108 108 text = options.delete(:text) || format_revision(revision)
109 109 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 110 link_to(
111 111 h(text),
112 112 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
113 113 :title => l(:label_revision_id, format_revision(revision))
114 114 )
115 115 end
116 116
117 117 # Generates a link to a message
118 118 def link_to_message(message, options={}, html_options = nil)
119 119 link_to(
120 120 truncate(message.subject, :length => 60),
121 121 board_message_path(message.board_id, message.parent_id || message.id, {
122 122 :r => (message.parent_id && message.id),
123 123 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 124 }.merge(options)),
125 125 html_options
126 126 )
127 127 end
128 128
129 129 # Generates a link to a project if active
130 130 # Examples:
131 131 #
132 132 # link_to_project(project) # => link to the specified project overview
133 133 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
134 134 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
135 135 #
136 136 def link_to_project(project, options={}, html_options = nil)
137 137 if project.archived?
138 138 h(project.name)
139 139 elsif options.key?(:action)
140 140 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
141 141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 142 link_to project.name, url, html_options
143 143 else
144 144 link_to project.name, project_path(project, options), html_options
145 145 end
146 146 end
147 147
148 148 # Generates a link to a project settings if active
149 149 def link_to_project_settings(project, options={}, html_options=nil)
150 150 if project.active?
151 151 link_to project.name, settings_project_path(project, options), html_options
152 152 elsif project.archived?
153 153 h(project.name)
154 154 else
155 155 link_to project.name, project_path(project, options), html_options
156 156 end
157 157 end
158 158
159 159 # Helper that formats object for html or text rendering
160 160 def format_object(object, html=true)
161 161 case object.class.name
162 162 when 'Array'
163 163 object.map {|o| format_object(o, html)}.join(', ').html_safe
164 164 when 'Time'
165 165 format_time(object)
166 166 when 'Date'
167 167 format_date(object)
168 168 when 'Fixnum'
169 169 object.to_s
170 170 when 'Float'
171 171 sprintf "%.2f", object
172 172 when 'User'
173 173 html ? link_to_user(object) : object.to_s
174 174 when 'Project'
175 175 html ? link_to_project(object) : object.to_s
176 176 when 'Version'
177 177 html ? link_to(object.name, version_path(object)) : object.to_s
178 178 when 'TrueClass'
179 179 l(:general_text_Yes)
180 180 when 'FalseClass'
181 181 l(:general_text_No)
182 182 when 'Issue'
183 183 object.visible? && html ? link_to_issue(object) : "##{object.id}"
184 184 when 'CustomValue', 'CustomFieldValue'
185 185 if object.custom_field
186 186 f = object.custom_field.format.formatted_custom_value(self, object, html)
187 187 if f.nil? || f.is_a?(String)
188 188 f
189 189 else
190 190 format_object(f, html)
191 191 end
192 192 else
193 193 object.value.to_s
194 194 end
195 195 else
196 196 html ? h(object) : object.to_s
197 197 end
198 198 end
199 199
200 200 def wiki_page_path(page, options={})
201 201 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
202 202 end
203 203
204 204 def thumbnail_tag(attachment)
205 205 link_to image_tag(thumbnail_path(attachment)),
206 206 named_attachment_path(attachment, attachment.filename),
207 207 :title => attachment.filename
208 208 end
209 209
210 210 def toggle_link(name, id, options={})
211 211 onclick = "$('##{id}').toggle(); "
212 212 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
213 213 onclick << "return false;"
214 214 link_to(name, "#", :onclick => onclick)
215 215 end
216 216
217 217 def image_to_function(name, function, html_options = {})
218 218 html_options.symbolize_keys!
219 219 tag(:input, html_options.merge({
220 220 :type => "image", :src => image_path(name),
221 221 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
222 222 }))
223 223 end
224 224
225 225 def format_activity_title(text)
226 226 h(truncate_single_line(text, :length => 100))
227 227 end
228 228
229 229 def format_activity_day(date)
230 230 date == User.current.today ? l(:label_today).titleize : format_date(date)
231 231 end
232 232
233 233 def format_activity_description(text)
234 234 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
235 235 ).gsub(/[\r\n]+/, "<br />").html_safe
236 236 end
237 237
238 238 def format_version_name(version)
239 239 if version.project == @project
240 240 h(version)
241 241 else
242 242 h("#{version.project} - #{version}")
243 243 end
244 244 end
245 245
246 246 def due_date_distance_in_words(date)
247 247 if date
248 248 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
249 249 end
250 250 end
251 251
252 252 # Renders a tree of projects as a nested set of unordered lists
253 253 # The given collection may be a subset of the whole project tree
254 254 # (eg. some intermediate nodes are private and can not be seen)
255 255 def render_project_nested_lists(projects)
256 256 s = ''
257 257 if projects.any?
258 258 ancestors = []
259 259 original_project = @project
260 260 projects.sort_by(&:lft).each do |project|
261 261 # set the project environment to please macros.
262 262 @project = project
263 263 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
264 264 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
265 265 else
266 266 ancestors.pop
267 267 s << "</li>"
268 268 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
269 269 ancestors.pop
270 270 s << "</ul></li>\n"
271 271 end
272 272 end
273 273 classes = (ancestors.empty? ? 'root' : 'child')
274 274 s << "<li class='#{classes}'><div class='#{classes}'>"
275 275 s << h(block_given? ? yield(project) : project.name)
276 276 s << "</div>\n"
277 277 ancestors << project
278 278 end
279 279 s << ("</li></ul>\n" * ancestors.size)
280 280 @project = original_project
281 281 end
282 282 s.html_safe
283 283 end
284 284
285 285 def render_page_hierarchy(pages, node=nil, options={})
286 286 content = ''
287 287 if pages[node]
288 288 content << "<ul class=\"pages-hierarchy\">\n"
289 289 pages[node].each do |page|
290 290 content << "<li>"
291 291 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
292 292 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
293 293 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
294 294 content << "</li>\n"
295 295 end
296 296 content << "</ul>\n"
297 297 end
298 298 content.html_safe
299 299 end
300 300
301 301 # Renders flash messages
302 302 def render_flash_messages
303 303 s = ''
304 304 flash.each do |k,v|
305 305 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
306 306 end
307 307 s.html_safe
308 308 end
309 309
310 310 # Renders tabs and their content
311 311 def render_tabs(tabs)
312 312 if tabs.any?
313 313 render :partial => 'common/tabs', :locals => {:tabs => tabs}
314 314 else
315 315 content_tag 'p', l(:label_no_data), :class => "nodata"
316 316 end
317 317 end
318 318
319 319 # Renders the project quick-jump box
320 320 def render_project_jump_box
321 321 return unless User.current.logged?
322 322 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
323 323 if projects.any?
324 324 options =
325 325 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
326 326 '<option value="" disabled="disabled">---</option>').html_safe
327 327
328 328 options << project_tree_options_for_select(projects, :selected => @project) do |p|
329 329 { :value => project_path(:id => p, :jump => current_menu_item) }
330 330 end
331 331
332 332 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
333 333 end
334 334 end
335 335
336 336 def project_tree_options_for_select(projects, options = {})
337 337 s = ''
338 338 project_tree(projects) do |project, level|
339 339 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
340 340 tag_options = {:value => project.id}
341 341 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
342 342 tag_options[:selected] = 'selected'
343 343 else
344 344 tag_options[:selected] = nil
345 345 end
346 346 tag_options.merge!(yield(project)) if block_given?
347 347 s << content_tag('option', name_prefix + h(project), tag_options)
348 348 end
349 349 s.html_safe
350 350 end
351 351
352 352 # Yields the given block for each project with its level in the tree
353 353 #
354 354 # Wrapper for Project#project_tree
355 355 def project_tree(projects, &block)
356 356 Project.project_tree(projects, &block)
357 357 end
358 358
359 359 def principals_check_box_tags(name, principals)
360 360 s = ''
361 361 principals.each do |principal|
362 362 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
363 363 end
364 364 s.html_safe
365 365 end
366 366
367 367 # Returns a string for users/groups option tags
368 368 def principals_options_for_select(collection, selected=nil)
369 369 s = ''
370 370 if collection.include?(User.current)
371 371 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
372 372 end
373 373 groups = ''
374 374 collection.sort.each do |element|
375 375 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
376 376 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
377 377 end
378 378 unless groups.empty?
379 379 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
380 380 end
381 381 s.html_safe
382 382 end
383 383
384 384 # Options for the new membership projects combo-box
385 385 def options_for_membership_project_select(principal, projects)
386 386 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
387 387 options << project_tree_options_for_select(projects) do |p|
388 388 {:disabled => principal.projects.to_a.include?(p)}
389 389 end
390 390 options
391 391 end
392 392
393 393 def option_tag(name, text, value, selected=nil, options={})
394 394 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
395 395 end
396 396
397 397 # Truncates and returns the string as a single line
398 398 def truncate_single_line(string, *args)
399 399 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
400 400 end
401 401
402 402 # Truncates at line break after 250 characters or options[:length]
403 403 def truncate_lines(string, options={})
404 404 length = options[:length] || 250
405 405 if string.to_s =~ /\A(.{#{length}}.*?)$/m
406 406 "#{$1}..."
407 407 else
408 408 string
409 409 end
410 410 end
411 411
412 412 def anchor(text)
413 413 text.to_s.gsub(' ', '_')
414 414 end
415 415
416 416 def html_hours(text)
417 417 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
418 418 end
419 419
420 420 def authoring(created, author, options={})
421 421 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
422 422 end
423 423
424 424 def time_tag(time)
425 425 text = distance_of_time_in_words(Time.now, time)
426 426 if @project
427 427 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
428 428 else
429 429 content_tag('abbr', text, :title => format_time(time))
430 430 end
431 431 end
432 432
433 433 def syntax_highlight_lines(name, content)
434 434 lines = []
435 435 syntax_highlight(name, content).each_line { |line| lines << line }
436 436 lines
437 437 end
438 438
439 439 def syntax_highlight(name, content)
440 440 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
441 441 end
442 442
443 443 def to_path_param(path)
444 444 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
445 445 str.blank? ? nil : str
446 446 end
447 447
448 448 def reorder_links(name, url, method = :post)
449 449 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
450 450 url.merge({"#{name}[move_to]" => 'highest'}),
451 451 :method => method, :title => l(:label_sort_highest)) +
452 452 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
453 453 url.merge({"#{name}[move_to]" => 'higher'}),
454 454 :method => method, :title => l(:label_sort_higher)) +
455 455 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
456 456 url.merge({"#{name}[move_to]" => 'lower'}),
457 457 :method => method, :title => l(:label_sort_lower)) +
458 458 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
459 459 url.merge({"#{name}[move_to]" => 'lowest'}),
460 460 :method => method, :title => l(:label_sort_lowest))
461 461 end
462 462
463 463 def breadcrumb(*args)
464 464 elements = args.flatten
465 465 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
466 466 end
467 467
468 468 def other_formats_links(&block)
469 469 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
470 470 yield Redmine::Views::OtherFormatsBuilder.new(self)
471 471 concat('</p>'.html_safe)
472 472 end
473 473
474 474 def page_header_title
475 475 if @project.nil? || @project.new_record?
476 476 h(Setting.app_title)
477 477 else
478 478 b = []
479 479 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
480 480 if ancestors.any?
481 481 root = ancestors.shift
482 482 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
483 483 if ancestors.size > 2
484 484 b << "\xe2\x80\xa6"
485 485 ancestors = ancestors[-2, 2]
486 486 end
487 487 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
488 488 end
489 489 b << h(@project)
490 490 b.join(" \xc2\xbb ").html_safe
491 491 end
492 492 end
493 493
494 494 # Returns a h2 tag and sets the html title with the given arguments
495 495 def title(*args)
496 496 strings = args.map do |arg|
497 497 if arg.is_a?(Array) && arg.size >= 2
498 498 link_to(*arg)
499 499 else
500 500 h(arg.to_s)
501 501 end
502 502 end
503 503 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
504 504 content_tag('h2', strings.join(' &#187; ').html_safe)
505 505 end
506 506
507 507 # Sets the html title
508 508 # Returns the html title when called without arguments
509 509 # Current project name and app_title and automatically appended
510 510 # Exemples:
511 511 # html_title 'Foo', 'Bar'
512 512 # html_title # => 'Foo - Bar - My Project - Redmine'
513 513 def html_title(*args)
514 514 if args.empty?
515 515 title = @html_title || []
516 516 title << @project.name if @project
517 517 title << Setting.app_title unless Setting.app_title == title.last
518 518 title.reject(&:blank?).join(' - ')
519 519 else
520 520 @html_title ||= []
521 521 @html_title += args
522 522 end
523 523 end
524 524
525 525 # Returns the theme, controller name, and action as css classes for the
526 526 # HTML body.
527 527 def body_css_classes
528 528 css = []
529 529 if theme = Redmine::Themes.theme(Setting.ui_theme)
530 530 css << 'theme-' + theme.name
531 531 end
532 532
533 533 css << 'project-' + @project.identifier if @project && @project.identifier.present?
534 534 css << 'controller-' + controller_name
535 535 css << 'action-' + action_name
536 536 css.join(' ')
537 537 end
538 538
539 539 def accesskey(s)
540 540 @used_accesskeys ||= []
541 541 key = Redmine::AccessKeys.key_for(s)
542 542 return nil if @used_accesskeys.include?(key)
543 543 @used_accesskeys << key
544 544 key
545 545 end
546 546
547 547 # Formats text according to system settings.
548 548 # 2 ways to call this method:
549 549 # * with a String: textilizable(text, options)
550 550 # * with an object and one of its attribute: textilizable(issue, :description, options)
551 551 def textilizable(*args)
552 552 options = args.last.is_a?(Hash) ? args.pop : {}
553 553 case args.size
554 554 when 1
555 555 obj = options[:object]
556 556 text = args.shift
557 557 when 2
558 558 obj = args.shift
559 559 attr = args.shift
560 560 text = obj.send(attr).to_s
561 561 else
562 562 raise ArgumentError, 'invalid arguments to textilizable'
563 563 end
564 564 return '' if text.blank?
565 565 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
566 566 only_path = options.delete(:only_path) == false ? false : true
567 567
568 568 text = text.dup
569 569 macros = catch_macros(text)
570 570 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
571 571
572 572 @parsed_headings = []
573 573 @heading_anchors = {}
574 574 @current_section = 0 if options[:edit_section_links]
575 575
576 576 parse_sections(text, project, obj, attr, only_path, options)
577 577 text = parse_non_pre_blocks(text, obj, macros) do |text|
578 578 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
579 579 send method_name, text, project, obj, attr, only_path, options
580 580 end
581 581 end
582 582 parse_headings(text, project, obj, attr, only_path, options)
583 583
584 584 if @parsed_headings.any?
585 585 replace_toc(text, @parsed_headings)
586 586 end
587 587
588 588 text.html_safe
589 589 end
590 590
591 591 def parse_non_pre_blocks(text, obj, macros)
592 592 s = StringScanner.new(text)
593 593 tags = []
594 594 parsed = ''
595 595 while !s.eos?
596 596 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
597 597 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
598 598 if tags.empty?
599 599 yield text
600 600 inject_macros(text, obj, macros) if macros.any?
601 601 else
602 602 inject_macros(text, obj, macros, false) if macros.any?
603 603 end
604 604 parsed << text
605 605 if tag
606 606 if closing
607 607 if tags.last == tag.downcase
608 608 tags.pop
609 609 end
610 610 else
611 611 tags << tag.downcase
612 612 end
613 613 parsed << full_tag
614 614 end
615 615 end
616 616 # Close any non closing tags
617 617 while tag = tags.pop
618 618 parsed << "</#{tag}>"
619 619 end
620 620 parsed
621 621 end
622 622
623 623 def parse_inline_attachments(text, project, obj, attr, only_path, options)
624 624 # when using an image link, try to use an attachment, if possible
625 625 attachments = options[:attachments] || []
626 626 attachments += obj.attachments if obj.respond_to?(:attachments)
627 627 if attachments.present?
628 628 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
629 629 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
630 630 # search for the picture in attachments
631 631 if found = Attachment.latest_attach(attachments, filename)
632 632 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
633 633 desc = found.description.to_s.gsub('"', '')
634 634 if !desc.blank? && alttext.blank?
635 635 alt = " title=\"#{desc}\" alt=\"#{desc}\""
636 636 end
637 637 "src=\"#{image_url}\"#{alt}"
638 638 else
639 639 m
640 640 end
641 641 end
642 642 end
643 643 end
644 644
645 645 # Wiki links
646 646 #
647 647 # Examples:
648 648 # [[mypage]]
649 649 # [[mypage|mytext]]
650 650 # wiki links can refer other project wikis, using project name or identifier:
651 651 # [[project:]] -> wiki starting page
652 652 # [[project:|mytext]]
653 653 # [[project:mypage]]
654 654 # [[project:mypage|mytext]]
655 655 def parse_wiki_links(text, project, obj, attr, only_path, options)
656 656 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
657 657 link_project = project
658 658 esc, all, page, title = $1, $2, $3, $5
659 659 if esc.nil?
660 660 if page =~ /^([^\:]+)\:(.*)$/
661 661 identifier, page = $1, $2
662 662 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
663 663 title ||= identifier if page.blank?
664 664 end
665 665
666 666 if link_project && link_project.wiki
667 667 # extract anchor
668 668 anchor = nil
669 669 if page =~ /^(.+?)\#(.+)$/
670 670 page, anchor = $1, $2
671 671 end
672 672 anchor = sanitize_anchor_name(anchor) if anchor.present?
673 673 # check if page exists
674 674 wiki_page = link_project.wiki.find_page(page)
675 675 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
676 676 "##{anchor}"
677 677 else
678 678 case options[:wiki_links]
679 679 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
680 680 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
681 681 else
682 682 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
683 683 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
684 684 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
685 685 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
686 686 end
687 687 end
688 688 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
689 689 else
690 690 # project or wiki doesn't exist
691 691 all
692 692 end
693 693 else
694 694 all
695 695 end
696 696 end
697 697 end
698 698
699 699 # Redmine links
700 700 #
701 701 # Examples:
702 702 # Issues:
703 703 # #52 -> Link to issue #52
704 704 # Changesets:
705 705 # r52 -> Link to revision 52
706 706 # commit:a85130f -> Link to scmid starting with a85130f
707 707 # Documents:
708 708 # document#17 -> Link to document with id 17
709 709 # document:Greetings -> Link to the document with title "Greetings"
710 710 # document:"Some document" -> Link to the document with title "Some document"
711 711 # Versions:
712 712 # version#3 -> Link to version with id 3
713 713 # version:1.0.0 -> Link to version named "1.0.0"
714 714 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
715 715 # Attachments:
716 716 # attachment:file.zip -> Link to the attachment of the current object named file.zip
717 717 # Source files:
718 718 # source:some/file -> Link to the file located at /some/file in the project's repository
719 719 # source:some/file@52 -> Link to the file's revision 52
720 720 # source:some/file#L120 -> Link to line 120 of the file
721 721 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
722 722 # export:some/file -> Force the download of the file
723 723 # Forum messages:
724 724 # message#1218 -> Link to message with id 1218
725 725 # Projects:
726 726 # project:someproject -> Link to project named "someproject"
727 727 # project#3 -> Link to project with id 3
728 728 #
729 729 # Links can refer other objects from other projects, using project identifier:
730 730 # identifier:r52
731 731 # identifier:document:"Some document"
732 732 # identifier:version:1.0.0
733 733 # identifier:source:some/file
734 734 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
735 735 text.gsub!(%r{([\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|
736 736 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
737 737 link = nil
738 738 project = default_project
739 739 if project_identifier
740 740 project = Project.visible.find_by_identifier(project_identifier)
741 741 end
742 742 if esc.nil?
743 743 if prefix.nil? && sep == 'r'
744 744 if project
745 745 repository = nil
746 746 if repo_identifier
747 747 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
748 748 else
749 749 repository = project.repository
750 750 end
751 751 # project.changesets.visible raises an SQL error because of a double join on repositories
752 752 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
753 753 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
754 754 :class => 'changeset',
755 755 :title => truncate_single_line(changeset.comments, :length => 100))
756 756 end
757 757 end
758 758 elsif sep == '#'
759 759 oid = identifier.to_i
760 760 case prefix
761 761 when nil
762 762 if oid.to_s == identifier &&
763 763 issue = Issue.visible.includes(:status).find_by_id(oid)
764 764 anchor = comment_id ? "note-#{comment_id}" : nil
765 765 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
766 766 :class => issue.css_classes,
767 767 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
768 768 end
769 769 when 'document'
770 770 if document = Document.visible.find_by_id(oid)
771 771 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
772 772 :class => 'document'
773 773 end
774 774 when 'version'
775 775 if version = Version.visible.find_by_id(oid)
776 776 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
777 777 :class => 'version'
778 778 end
779 779 when 'message'
780 780 if message = Message.visible.includes(:parent).find_by_id(oid)
781 781 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
782 782 end
783 783 when 'forum'
784 784 if board = Board.visible.find_by_id(oid)
785 785 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
786 786 :class => 'board'
787 787 end
788 788 when 'news'
789 789 if news = News.visible.find_by_id(oid)
790 790 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
791 791 :class => 'news'
792 792 end
793 793 when 'project'
794 794 if p = Project.visible.find_by_id(oid)
795 795 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
796 796 end
797 797 end
798 798 elsif sep == ':'
799 799 # removes the double quotes if any
800 800 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
801 801 case prefix
802 802 when 'document'
803 803 if project && document = project.documents.visible.find_by_title(name)
804 804 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
805 805 :class => 'document'
806 806 end
807 807 when 'version'
808 808 if project && version = project.versions.visible.find_by_name(name)
809 809 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
810 810 :class => 'version'
811 811 end
812 812 when 'forum'
813 813 if project && board = project.boards.visible.find_by_name(name)
814 814 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
815 815 :class => 'board'
816 816 end
817 817 when 'news'
818 818 if project && news = project.news.visible.find_by_title(name)
819 819 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
820 820 :class => 'news'
821 821 end
822 822 when 'commit', 'source', 'export'
823 823 if project
824 824 repository = nil
825 825 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
826 826 repo_prefix, repo_identifier, name = $1, $2, $3
827 827 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
828 828 else
829 829 repository = project.repository
830 830 end
831 831 if prefix == 'commit'
832 832 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
833 833 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},
834 834 :class => 'changeset',
835 835 :title => truncate_single_line(changeset.comments, :length => 100)
836 836 end
837 837 else
838 838 if repository && User.current.allowed_to?(:browse_repository, project)
839 839 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
840 840 path, rev, anchor = $1, $3, $5
841 841 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
842 842 :path => to_path_param(path),
843 843 :rev => rev,
844 844 :anchor => anchor},
845 845 :class => (prefix == 'export' ? 'source download' : 'source')
846 846 end
847 847 end
848 848 repo_prefix = nil
849 849 end
850 850 when 'attachment'
851 851 attachments = options[:attachments] || []
852 852 attachments += obj.attachments if obj.respond_to?(:attachments)
853 853 if attachments && attachment = Attachment.latest_attach(attachments, name)
854 854 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
855 855 end
856 856 when 'project'
857 857 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
858 858 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
859 859 end
860 860 end
861 861 end
862 862 end
863 863 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
864 864 end
865 865 end
866 866
867 867 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
868 868
869 869 def parse_sections(text, project, obj, attr, only_path, options)
870 870 return unless options[:edit_section_links]
871 871 text.gsub!(HEADING_RE) do
872 872 heading = $1
873 873 @current_section += 1
874 874 if @current_section > 1
875 875 content_tag('div',
876 876 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
877 877 :class => 'contextual',
878 878 :title => l(:button_edit_section),
879 879 :id => "section-#{@current_section}") + heading.html_safe
880 880 else
881 881 heading
882 882 end
883 883 end
884 884 end
885 885
886 886 # Headings and TOC
887 887 # Adds ids and links to headings unless options[:headings] is set to false
888 888 def parse_headings(text, project, obj, attr, only_path, options)
889 889 return if options[:headings] == false
890 890
891 891 text.gsub!(HEADING_RE) do
892 892 level, attrs, content = $2.to_i, $3, $4
893 893 item = strip_tags(content).strip
894 894 anchor = sanitize_anchor_name(item)
895 895 # used for single-file wiki export
896 896 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
897 897 @heading_anchors[anchor] ||= 0
898 898 idx = (@heading_anchors[anchor] += 1)
899 899 if idx > 1
900 900 anchor = "#{anchor}-#{idx}"
901 901 end
902 902 @parsed_headings << [level, anchor, item]
903 903 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
904 904 end
905 905 end
906 906
907 907 MACROS_RE = /(
908 908 (!)? # escaping
909 909 (
910 910 \{\{ # opening tag
911 911 ([\w]+) # macro name
912 912 (\(([^\n\r]*?)\))? # optional arguments
913 913 ([\n\r].*?[\n\r])? # optional block of text
914 914 \}\} # closing tag
915 915 )
916 916 )/mx unless const_defined?(:MACROS_RE)
917 917
918 918 MACRO_SUB_RE = /(
919 919 \{\{
920 920 macro\((\d+)\)
921 921 \}\}
922 922 )/x unless const_defined?(:MACRO_SUB_RE)
923 923
924 924 # Extracts macros from text
925 925 def catch_macros(text)
926 926 macros = {}
927 927 text.gsub!(MACROS_RE) do
928 928 all, macro = $1, $4.downcase
929 929 if macro_exists?(macro) || all =~ MACRO_SUB_RE
930 930 index = macros.size
931 931 macros[index] = all
932 932 "{{macro(#{index})}}"
933 933 else
934 934 all
935 935 end
936 936 end
937 937 macros
938 938 end
939 939
940 940 # Executes and replaces macros in text
941 941 def inject_macros(text, obj, macros, execute=true)
942 942 text.gsub!(MACRO_SUB_RE) do
943 943 all, index = $1, $2.to_i
944 944 orig = macros.delete(index)
945 945 if execute && orig && orig =~ MACROS_RE
946 946 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
947 947 if esc.nil?
948 948 h(exec_macro(macro, obj, args, block) || all)
949 949 else
950 950 h(all)
951 951 end
952 952 elsif orig
953 953 h(orig)
954 954 else
955 955 h(all)
956 956 end
957 957 end
958 958 end
959 959
960 960 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
961 961
962 962 # Renders the TOC with given headings
963 963 def replace_toc(text, headings)
964 964 text.gsub!(TOC_RE) do
965 965 # Keep only the 4 first levels
966 966 headings = headings.select{|level, anchor, item| level <= 4}
967 967 if headings.empty?
968 968 ''
969 969 else
970 970 div_class = 'toc'
971 971 div_class << ' right' if $1 == '>'
972 972 div_class << ' left' if $1 == '<'
973 973 out = "<ul class=\"#{div_class}\"><li>"
974 974 root = headings.map(&:first).min
975 975 current = root
976 976 started = false
977 977 headings.each do |level, anchor, item|
978 978 if level > current
979 979 out << '<ul><li>' * (level - current)
980 980 elsif level < current
981 981 out << "</li></ul>\n" * (current - level) + "</li><li>"
982 982 elsif started
983 983 out << '</li><li>'
984 984 end
985 985 out << "<a href=\"##{anchor}\">#{item}</a>"
986 986 current = level
987 987 started = true
988 988 end
989 989 out << '</li></ul>' * (current - root)
990 990 out << '</li></ul>'
991 991 end
992 992 end
993 993 end
994 994
995 995 # Same as Rails' simple_format helper without using paragraphs
996 996 def simple_format_without_paragraph(text)
997 997 text.to_s.
998 998 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
999 999 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1000 1000 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1001 1001 html_safe
1002 1002 end
1003 1003
1004 1004 def lang_options_for_select(blank=true)
1005 1005 (blank ? [["(auto)", ""]] : []) + languages_options
1006 1006 end
1007 1007
1008 1008 def label_tag_for(name, option_tags = nil, options = {})
1009 1009 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1010 1010 content_tag("label", label_text)
1011 1011 end
1012 1012
1013 1013 def labelled_form_for(*args, &proc)
1014 1014 args << {} unless args.last.is_a?(Hash)
1015 1015 options = args.last
1016 1016 if args.first.is_a?(Symbol)
1017 1017 options.merge!(:as => args.shift)
1018 1018 end
1019 1019 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1020 1020 form_for(*args, &proc)
1021 1021 end
1022 1022
1023 1023 def labelled_fields_for(*args, &proc)
1024 1024 args << {} unless args.last.is_a?(Hash)
1025 1025 options = args.last
1026 1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 1027 fields_for(*args, &proc)
1028 1028 end
1029 1029
1030 1030 def labelled_remote_form_for(*args, &proc)
1031 1031 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1032 1032 args << {} unless args.last.is_a?(Hash)
1033 1033 options = args.last
1034 1034 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1035 1035 form_for(*args, &proc)
1036 1036 end
1037 1037
1038 1038 def error_messages_for(*objects)
1039 1039 html = ""
1040 1040 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1041 1041 errors = objects.map {|o| o.errors.full_messages}.flatten
1042 1042 if errors.any?
1043 1043 html << "<div id='errorExplanation'><ul>\n"
1044 1044 errors.each do |error|
1045 1045 html << "<li>#{h error}</li>\n"
1046 1046 end
1047 1047 html << "</ul></div>\n"
1048 1048 end
1049 1049 html.html_safe
1050 1050 end
1051 1051
1052 1052 def delete_link(url, options={})
1053 1053 options = {
1054 1054 :method => :delete,
1055 1055 :data => {:confirm => l(:text_are_you_sure)},
1056 1056 :class => 'icon icon-del'
1057 1057 }.merge(options)
1058 1058
1059 1059 link_to l(:button_delete), url, options
1060 1060 end
1061 1061
1062 1062 def preview_link(url, form, target='preview', options={})
1063 1063 content_tag 'a', l(:label_preview), {
1064 1064 :href => "#",
1065 1065 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1066 1066 :accesskey => accesskey(:preview)
1067 1067 }.merge(options)
1068 1068 end
1069 1069
1070 1070 def link_to_function(name, function, html_options={})
1071 1071 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1072 1072 end
1073 1073
1074 1074 # Helper to render JSON in views
1075 1075 def raw_json(arg)
1076 1076 arg.to_json.to_s.gsub('/', '\/').html_safe
1077 1077 end
1078 1078
1079 1079 def back_url
1080 1080 url = params[:back_url]
1081 1081 if url.nil? && referer = request.env['HTTP_REFERER']
1082 1082 url = CGI.unescape(referer.to_s)
1083 1083 end
1084 1084 url
1085 1085 end
1086 1086
1087 1087 def back_url_hidden_field_tag
1088 1088 url = back_url
1089 1089 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1090 1090 end
1091 1091
1092 1092 def check_all_links(form_name)
1093 1093 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1094 1094 " | ".html_safe +
1095 1095 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1096 1096 end
1097 1097
1098 1098 def progress_bar(pcts, options={})
1099 1099 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1100 1100 pcts = pcts.collect(&:round)
1101 1101 pcts[1] = pcts[1] - pcts[0]
1102 1102 pcts << (100 - pcts[1] - pcts[0])
1103 1103 width = options[:width] || '100px;'
1104 1104 legend = options[:legend] || ''
1105 1105 content_tag('table',
1106 1106 content_tag('tr',
1107 1107 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1108 1108 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1109 1109 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1110 1110 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1111 1111 content_tag('p', legend, :class => 'percent').html_safe
1112 1112 end
1113 1113
1114 1114 def checked_image(checked=true)
1115 1115 if checked
1116 1116 image_tag 'toggle_check.png'
1117 1117 end
1118 1118 end
1119 1119
1120 1120 def context_menu(url)
1121 1121 unless @context_menu_included
1122 1122 content_for :header_tags do
1123 1123 javascript_include_tag('context_menu') +
1124 1124 stylesheet_link_tag('context_menu')
1125 1125 end
1126 1126 if l(:direction) == 'rtl'
1127 1127 content_for :header_tags do
1128 1128 stylesheet_link_tag('context_menu_rtl')
1129 1129 end
1130 1130 end
1131 1131 @context_menu_included = true
1132 1132 end
1133 1133 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1134 1134 end
1135 1135
1136 1136 def calendar_for(field_id)
1137 1137 include_calendar_headers_tags
1138 1138 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1139 1139 end
1140 1140
1141 1141 def include_calendar_headers_tags
1142 1142 unless @calendar_headers_tags_included
1143 1143 tags = javascript_include_tag("datepicker")
1144 1144 @calendar_headers_tags_included = true
1145 1145 content_for :header_tags do
1146 1146 start_of_week = Setting.start_of_week
1147 1147 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1148 1148 # Redmine uses 1..7 (monday..sunday) in settings and locales
1149 1149 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1150 1150 start_of_week = start_of_week.to_i % 7
1151 1151 tags << javascript_tag(
1152 1152 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1153 1153 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1154 1154 path_to_image('/images/calendar.png') +
1155 1155 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1156 1156 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1157 1157 "beforeShow: beforeShowDatePicker};")
1158 1158 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1159 1159 unless jquery_locale == 'en'
1160 1160 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1161 1161 end
1162 1162 tags
1163 1163 end
1164 1164 end
1165 1165 end
1166 1166
1167 1167 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1168 1168 # Examples:
1169 1169 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1170 1170 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1171 1171 #
1172 1172 def stylesheet_link_tag(*sources)
1173 1173 options = sources.last.is_a?(Hash) ? sources.pop : {}
1174 1174 plugin = options.delete(:plugin)
1175 1175 sources = sources.map do |source|
1176 1176 if plugin
1177 1177 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1178 1178 elsif current_theme && current_theme.stylesheets.include?(source)
1179 1179 current_theme.stylesheet_path(source)
1180 1180 else
1181 1181 source
1182 1182 end
1183 1183 end
1184 1184 super sources, options
1185 1185 end
1186 1186
1187 1187 # Overrides Rails' image_tag with themes and plugins support.
1188 1188 # Examples:
1189 1189 # image_tag('image.png') # => picks image.png from the current theme or defaults
1190 1190 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1191 1191 #
1192 1192 def image_tag(source, options={})
1193 1193 if plugin = options.delete(:plugin)
1194 1194 source = "/plugin_assets/#{plugin}/images/#{source}"
1195 1195 elsif current_theme && current_theme.images.include?(source)
1196 1196 source = current_theme.image_path(source)
1197 1197 end
1198 1198 super source, options
1199 1199 end
1200 1200
1201 1201 # Overrides Rails' javascript_include_tag with plugins support
1202 1202 # Examples:
1203 1203 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1204 1204 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1205 1205 #
1206 1206 def javascript_include_tag(*sources)
1207 1207 options = sources.last.is_a?(Hash) ? sources.pop : {}
1208 1208 if plugin = options.delete(:plugin)
1209 1209 sources = sources.map do |source|
1210 1210 if plugin
1211 1211 "/plugin_assets/#{plugin}/javascripts/#{source}"
1212 1212 else
1213 1213 source
1214 1214 end
1215 1215 end
1216 1216 end
1217 1217 super sources, options
1218 1218 end
1219 1219
1220 1220 # TODO: remove this in 2.5.0
1221 1221 def has_content?(name)
1222 1222 content_for?(name)
1223 1223 end
1224 1224
1225 1225 def sidebar_content?
1226 1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 1227 end
1228 1228
1229 1229 def view_layouts_base_sidebar_hook_response
1230 1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 1231 end
1232 1232
1233 1233 def email_delivery_enabled?
1234 1234 !!ActionMailer::Base.perform_deliveries
1235 1235 end
1236 1236
1237 1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 1239 def avatar(user, options = { })
1240 1240 if Setting.gravatar_enabled?
1241 1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 1242 email = nil
1243 1243 if user.respond_to?(:mail)
1244 1244 email = user.mail
1245 1245 elsif user.to_s =~ %r{<(.+?)>}
1246 1246 email = $1
1247 1247 end
1248 1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 1249 else
1250 1250 ''
1251 1251 end
1252 1252 end
1253 1253
1254 1254 def sanitize_anchor_name(anchor)
1255 1255 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1256 1256 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1257 1257 else
1258 1258 # TODO: remove when ruby1.8 is no longer supported
1259 1259 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1260 1260 end
1261 1261 end
1262 1262
1263 1263 # Returns the javascript tags that are included in the html layout head
1264 1264 def javascript_heads
1265 1265 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1266 1266 unless User.current.pref.warn_on_leaving_unsaved == '0'
1267 1267 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1268 1268 end
1269 1269 tags
1270 1270 end
1271 1271
1272 1272 def favicon
1273 1273 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1274 1274 end
1275 1275
1276 1276 # Returns the path to the favicon
1277 1277 def favicon_path
1278 1278 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1279 1279 image_path(icon)
1280 1280 end
1281 1281
1282 1282 # Returns the full URL to the favicon
1283 1283 def favicon_url
1284 1284 # TODO: use #image_url introduced in Rails4
1285 1285 path = favicon_path
1286 1286 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1287 1287 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1288 1288 end
1289 1289
1290 1290 def robot_exclusion_tag
1291 1291 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1292 1292 end
1293 1293
1294 1294 # Returns true if arg is expected in the API response
1295 1295 def include_in_api_response?(arg)
1296 1296 unless @included_in_api_response
1297 1297 param = params[:include]
1298 1298 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1299 1299 @included_in_api_response.collect!(&:strip)
1300 1300 end
1301 1301 @included_in_api_response.include?(arg.to_s)
1302 1302 end
1303 1303
1304 1304 # Returns options or nil if nometa param or X-Redmine-Nometa header
1305 1305 # was set in the request
1306 1306 def api_meta(options)
1307 1307 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1308 1308 # compatibility mode for activeresource clients that raise
1309 1309 # an error when unserializing an array with attributes
1310 1310 nil
1311 1311 else
1312 1312 options
1313 1313 end
1314 1314 end
1315 1315
1316 1316 private
1317 1317
1318 1318 def wiki_helper
1319 1319 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1320 1320 extend helper
1321 1321 return self
1322 1322 end
1323 1323
1324 1324 def link_to_content_update(text, url_params = {}, html_options = {})
1325 1325 link_to(text, url_params, html_options)
1326 1326 end
1327 1327 end
@@ -1,47 +1,47
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 # Displays view/delete links to the attachments of the given object
22 22 # Options:
23 23 # :author -- author names are not displayed if set to false
24 24 # :thumbails -- display thumbnails if enabled in settings
25 25 def link_to_attachments(container, options = {})
26 26 options.assert_valid_keys(:author, :thumbnails)
27 27
28 28 if container.attachments.any?
29 29 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
30 30 render :partial => 'attachments/links',
31 31 :locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
32 32 end
33 33 end
34 34
35 35 def render_api_attachment(attachment, api)
36 36 api.attachment do
37 37 api.id attachment.id
38 38 api.filename attachment.filename
39 39 api.filesize attachment.filesize
40 40 api.content_type attachment.content_type
41 41 api.description attachment.description
42 42 api.content_url url_for(:controller => 'attachments', :action => 'download', :id => attachment, :filename => attachment.filename, :only_path => false)
43 43 api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
44 44 api.created_on attachment.created_on
45 45 end
46 46 end
47 47 end
@@ -1,24 +1,24
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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))
57 57 end
58 58 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 end
@@ -1,139 +1,139
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 => 'UserCustomField', :partial => 'custom_fields/index',
32 32 :label => :label_user_plural},
33 33 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
34 34 :label => :label_group_plural},
35 35 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
36 36 :label => TimeEntryActivity::OptionName},
37 37 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
38 38 :label => IssuePriority::OptionName},
39 39 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
40 40 :label => DocumentCategory::OptionName}
41 41 ]
42 42
43 43 def custom_fields_tabs
44 44 CUSTOM_FIELDS_TABS
45 45 end
46 46
47 47 def render_custom_field_format_partial(form, custom_field)
48 48 partial = custom_field.format.form_partial
49 49 if partial
50 50 render :partial => custom_field.format.form_partial, :locals => {:f => form, :custom_field => custom_field}
51 51 end
52 52 end
53 53
54 54 def custom_field_tag_name(prefix, custom_field)
55 55 name = "#{prefix}[custom_field_values][#{custom_field.id}]"
56 56 name << "[]" if custom_field.multiple?
57 57 name
58 58 end
59 59
60 60 def custom_field_tag_id(prefix, custom_field)
61 61 "#{prefix}_custom_field_values_#{custom_field.id}"
62 62 end
63 63
64 64 # Return custom field html tag corresponding to its format
65 65 def custom_field_tag(prefix, custom_value)
66 66 custom_value.custom_field.format.edit_tag self,
67 67 custom_field_tag_id(prefix, custom_value.custom_field),
68 68 custom_field_tag_name(prefix, custom_value.custom_field),
69 69 custom_value,
70 70 :class => "#{custom_value.custom_field.field_format}_cf"
71 71 end
72 72
73 73 # Return custom field label tag
74 74 def custom_field_label_tag(name, custom_value, options={})
75 75 required = options[:required] || custom_value.custom_field.is_required?
76 76 title = custom_value.custom_field.description.presence
77 77 content = content_tag 'span', custom_value.custom_field.name, :title => title
78 78
79 79 content_tag "label", content +
80 80 (required ? " <span class=\"required\">*</span>".html_safe : ""),
81 81 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
82 82 end
83 83
84 84 # Return custom field tag with its label tag
85 85 def custom_field_tag_with_label(name, custom_value, options={})
86 86 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
87 87 end
88 88
89 89 # Returns the custom field tag for when bulk editing objects
90 90 def custom_field_tag_for_bulk_edit(prefix, custom_field, objects=nil, value='')
91 91 custom_field.format.bulk_edit_tag self,
92 92 custom_field_tag_id(prefix, custom_field),
93 93 custom_field_tag_name(prefix, custom_field),
94 94 custom_field,
95 95 objects,
96 96 value,
97 97 :class => "#{custom_field.field_format}_cf"
98 98 end
99 99
100 100 # Return a string used to display a custom value
101 101 def show_value(custom_value, html=true)
102 102 format_object(custom_value, html)
103 103 end
104 104
105 105 # Return a string used to display a custom value
106 106 def format_value(value, custom_field)
107 107 format_object(custom_field.format.formatted_value(self, custom_field, value, false), false)
108 108 end
109 109
110 110 # Return an array of custom field formats which can be used in select_tag
111 111 def custom_field_formats_for_select(custom_field)
112 112 Redmine::FieldFormat.as_select(custom_field.class.customized_class.name)
113 113 end
114 114
115 115 # Renders the custom_values in api views
116 116 def render_api_custom_values(custom_values, api)
117 117 api.array :custom_fields do
118 118 custom_values.each do |custom_value|
119 119 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
120 120 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
121 121 api.custom_field attrs do
122 122 if custom_value.value.is_a?(Array)
123 123 api.array :value do
124 124 custom_value.value.each do |value|
125 125 api.value value unless value.blank?
126 126 end
127 127 end
128 128 else
129 129 api.value custom_value.value
130 130 end
131 131 end
132 132 end
133 133 end unless custom_values.empty?
134 134 end
135 135
136 136 def edit_tag_style_tag(form)
137 137 form.select :edit_tag_style, [[l(:label_drop_down_list), ''], [l(:label_checkboxes), 'check_box']], :label => :label_display
138 138 end
139 139 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,42 +1,42
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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
22 22 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
23 23 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
24 24 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
25 25 ]
26 26 end
27 27
28 28 def render_principals_for_new_group_users(group)
29 29 scope = User.active.sorted.not_in_group(group).like(params[:q])
30 30 principal_count = scope.count
31 31 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
32 32 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
33 33
34 34 s = content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals')
35 35
36 36 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
37 37 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
38 38 }
39 39
40 40 s + content_tag('p', links, :class => 'pagination')
41 41 end
42 42 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,430 +1,430
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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
23 23 def issue_list(issues, &block)
24 24 ancestors = []
25 25 issues.each do |issue|
26 26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 27 ancestors.pop
28 28 end
29 29 yield issue, ancestors.size
30 30 ancestors << issue unless issue.leaf?
31 31 end
32 32 end
33 33
34 34 # Renders a HTML/CSS tooltip
35 35 #
36 36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 37 # that contains this method wrapped in a span with the class of "tip"
38 38 #
39 39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 41 # </div>
42 42 #
43 43 def render_issue_tooltip(issue)
44 44 @cached_label_status ||= l(:field_status)
45 45 @cached_label_start_date ||= l(:field_start_date)
46 46 @cached_label_due_date ||= l(:field_due_date)
47 47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 48 @cached_label_priority ||= l(:field_priority)
49 49 @cached_label_project ||= l(:field_project)
50 50
51 51 link_to_issue(issue) + "<br /><br />".html_safe +
52 52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 58 end
59 59
60 60 def issue_heading(issue)
61 61 h("#{issue.tracker} ##{issue.id}")
62 62 end
63 63
64 64 def render_issue_subject_with_tree(issue)
65 65 s = ''
66 66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 67 ancestors.each do |ancestor|
68 68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 69 end
70 70 s << '<div>'
71 71 subject = h(issue.subject)
72 72 if issue.is_private?
73 73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 74 end
75 75 s << content_tag('h3', subject)
76 76 s << '</div>' * (ancestors.size + 1)
77 77 s.html_safe
78 78 end
79 79
80 80 def render_descendants_tree(issue)
81 81 s = '<form><table class="list issues">'
82 82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 83 css = "issue issue-#{child.id} hascontextmenu"
84 84 css << " idnt idnt-#{level}" if level > 0
85 85 s << content_tag('tr',
86 86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 88 content_tag('td', h(child.status)) +
89 89 content_tag('td', link_to_user(child.assigned_to)) +
90 90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 91 :class => css)
92 92 end
93 93 s << '</table></form>'
94 94 s.html_safe
95 95 end
96 96
97 97 # Returns an array of error messages for bulk edited issues
98 98 def bulk_edit_error_messages(issues)
99 99 messages = {}
100 100 issues.each do |issue|
101 101 issue.errors.full_messages.each do |message|
102 102 messages[message] ||= []
103 103 messages[message] << issue
104 104 end
105 105 end
106 106 messages.map { |message, issues|
107 107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 108 }
109 109 end
110 110
111 111 # Returns a link for adding a new subtask to the given issue
112 112 def link_to_new_subtask(issue)
113 113 attrs = {
114 114 :tracker_id => issue.tracker,
115 115 :parent_issue_id => issue
116 116 }
117 117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 118 end
119 119
120 120 class IssueFieldsRows
121 121 include ActionView::Helpers::TagHelper
122 122
123 123 def initialize
124 124 @left = []
125 125 @right = []
126 126 end
127 127
128 128 def left(*args)
129 129 args.any? ? @left << cells(*args) : @left
130 130 end
131 131
132 132 def right(*args)
133 133 args.any? ? @right << cells(*args) : @right
134 134 end
135 135
136 136 def size
137 137 @left.size > @right.size ? @left.size : @right.size
138 138 end
139 139
140 140 def to_html
141 141 html = ''.html_safe
142 142 blank = content_tag('th', '') + content_tag('td', '')
143 143 size.times do |i|
144 144 left = @left[i] || blank
145 145 right = @right[i] || blank
146 146 html << content_tag('tr', left + right)
147 147 end
148 148 html
149 149 end
150 150
151 151 def cells(label, text, options={})
152 152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 153 end
154 154 end
155 155
156 156 def issue_fields_rows
157 157 r = IssueFieldsRows.new
158 158 yield r
159 159 r.to_html
160 160 end
161 161
162 162 def render_custom_fields_rows(issue)
163 163 values = issue.visible_custom_field_values
164 164 return if values.empty?
165 165 ordered_values = []
166 166 half = (values.size / 2.0).ceil
167 167 half.times do |i|
168 168 ordered_values << values[i]
169 169 ordered_values << values[i + half]
170 170 end
171 171 s = "<tr>\n"
172 172 n = 0
173 173 ordered_values.compact.each do |value|
174 174 css = "cf_#{value.custom_field.id}"
175 175 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
176 176 s << "\t<th class=\"#{css}\">#{ h(value.custom_field.name) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n"
177 177 n += 1
178 178 end
179 179 s << "</tr>\n"
180 180 s.html_safe
181 181 end
182 182
183 183 def issues_destroy_confirmation_message(issues)
184 184 issues = [issues] unless issues.is_a?(Array)
185 185 message = l(:text_issues_destroy_confirmation)
186 186 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
187 187 if descendant_count > 0
188 188 issues.each do |issue|
189 189 next if issue.root?
190 190 issues.each do |other_issue|
191 191 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
192 192 end
193 193 end
194 194 if descendant_count > 0
195 195 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
196 196 end
197 197 end
198 198 message
199 199 end
200 200
201 201 def sidebar_queries
202 202 unless @sidebar_queries
203 203 @sidebar_queries = IssueQuery.visible.
204 204 order("#{Query.table_name}.name ASC").
205 205 # Project specific queries and global queries
206 206 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
207 207 all
208 208 end
209 209 @sidebar_queries
210 210 end
211 211
212 212 def query_links(title, queries)
213 213 return '' if queries.empty?
214 214 # links to #index on issues/show
215 215 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
216 216
217 217 content_tag('h3', title) + "\n" +
218 218 content_tag('ul',
219 219 queries.collect {|query|
220 220 css = 'query'
221 221 css << ' selected' if query == @query
222 222 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
223 223 }.join("\n").html_safe,
224 224 :class => 'queries'
225 225 ) + "\n"
226 226 end
227 227
228 228 def render_sidebar_queries
229 229 out = ''.html_safe
230 230 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
231 231 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
232 232 out
233 233 end
234 234
235 235 def email_issue_attributes(issue, user)
236 236 items = []
237 237 %w(author status priority assigned_to category fixed_version).each do |attribute|
238 238 unless issue.disabled_core_fields.include?(attribute+"_id")
239 239 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
240 240 end
241 241 end
242 242 issue.visible_custom_field_values(user).each do |value|
243 243 items << "#{value.custom_field.name}: #{show_value(value, false)}"
244 244 end
245 245 items
246 246 end
247 247
248 248 def render_email_issue_attributes(issue, user, html=false)
249 249 items = email_issue_attributes(issue, user)
250 250 if html
251 251 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
252 252 else
253 253 items.map{|s| "* #{s}"}.join("\n")
254 254 end
255 255 end
256 256
257 257 # Returns the textual representation of a journal details
258 258 # as an array of strings
259 259 def details_to_strings(details, no_html=false, options={})
260 260 options[:only_path] = (options[:only_path] == false ? false : true)
261 261 strings = []
262 262 values_by_field = {}
263 263 details.each do |detail|
264 264 if detail.property == 'cf'
265 265 field = detail.custom_field
266 266 if field && field.multiple?
267 267 values_by_field[field] ||= {:added => [], :deleted => []}
268 268 if detail.old_value
269 269 values_by_field[field][:deleted] << detail.old_value
270 270 end
271 271 if detail.value
272 272 values_by_field[field][:added] << detail.value
273 273 end
274 274 next
275 275 end
276 276 end
277 277 strings << show_detail(detail, no_html, options)
278 278 end
279 279 values_by_field.each do |field, changes|
280 280 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
281 281 detail.instance_variable_set "@custom_field", field
282 282 if changes[:added].any?
283 283 detail.value = changes[:added]
284 284 strings << show_detail(detail, no_html, options)
285 285 elsif changes[:deleted].any?
286 286 detail.old_value = changes[:deleted]
287 287 strings << show_detail(detail, no_html, options)
288 288 end
289 289 end
290 290 strings
291 291 end
292 292
293 293 # Returns the textual representation of a single journal detail
294 294 def show_detail(detail, no_html=false, options={})
295 295 multiple = false
296 296 case detail.property
297 297 when 'attr'
298 298 field = detail.prop_key.to_s.gsub(/\_id$/, "")
299 299 label = l(("field_" + field).to_sym)
300 300 case detail.prop_key
301 301 when 'due_date', 'start_date'
302 302 value = format_date(detail.value.to_date) if detail.value
303 303 old_value = format_date(detail.old_value.to_date) if detail.old_value
304 304
305 305 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
306 306 'priority_id', 'category_id', 'fixed_version_id'
307 307 value = find_name_by_reflection(field, detail.value)
308 308 old_value = find_name_by_reflection(field, detail.old_value)
309 309
310 310 when 'estimated_hours'
311 311 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
312 312 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
313 313
314 314 when 'parent_id'
315 315 label = l(:field_parent_issue)
316 316 value = "##{detail.value}" unless detail.value.blank?
317 317 old_value = "##{detail.old_value}" unless detail.old_value.blank?
318 318
319 319 when 'is_private'
320 320 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
321 321 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
322 322 end
323 323 when 'cf'
324 324 custom_field = detail.custom_field
325 325 if custom_field
326 326 multiple = custom_field.multiple?
327 327 label = custom_field.name
328 328 value = format_value(detail.value, custom_field) if detail.value
329 329 old_value = format_value(detail.old_value, custom_field) if detail.old_value
330 330 end
331 331 when 'attachment'
332 332 label = l(:label_attachment)
333 333 when 'relation'
334 334 if detail.value && !detail.old_value
335 335 rel_issue = Issue.visible.find_by_id(detail.value)
336 336 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
337 337 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
338 338 elsif detail.old_value && !detail.value
339 339 rel_issue = Issue.visible.find_by_id(detail.old_value)
340 340 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
341 341 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
342 342 end
343 343 relation_type = IssueRelation::TYPES[detail.prop_key]
344 344 label = l(relation_type[:name]) if relation_type
345 345 end
346 346 call_hook(:helper_issues_show_detail_after_setting,
347 347 {:detail => detail, :label => label, :value => value, :old_value => old_value })
348 348
349 349 label ||= detail.prop_key
350 350 value ||= detail.value
351 351 old_value ||= detail.old_value
352 352
353 353 unless no_html
354 354 label = content_tag('strong', label)
355 355 old_value = content_tag("i", h(old_value)) if detail.old_value
356 356 if detail.old_value && detail.value.blank? && detail.property != 'relation'
357 357 old_value = content_tag("del", old_value)
358 358 end
359 359 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
360 360 # Link to the attachment if it has not been removed
361 361 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
362 362 if options[:only_path] != false && atta.is_text?
363 363 value += link_to(
364 364 image_tag('magnifier.png'),
365 365 :controller => 'attachments', :action => 'show',
366 366 :id => atta, :filename => atta.filename
367 367 )
368 368 end
369 369 else
370 370 value = content_tag("i", h(value)) if value
371 371 end
372 372 end
373 373
374 374 if detail.property == 'attr' && detail.prop_key == 'description'
375 375 s = l(:text_journal_changed_no_detail, :label => label)
376 376 unless no_html
377 377 diff_link = link_to 'diff',
378 378 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
379 379 :detail_id => detail.id, :only_path => options[:only_path]},
380 380 :title => l(:label_view_diff)
381 381 s << " (#{ diff_link })"
382 382 end
383 383 s.html_safe
384 384 elsif detail.value.present?
385 385 case detail.property
386 386 when 'attr', 'cf'
387 387 if detail.old_value.present?
388 388 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
389 389 elsif multiple
390 390 l(:text_journal_added, :label => label, :value => value).html_safe
391 391 else
392 392 l(:text_journal_set_to, :label => label, :value => value).html_safe
393 393 end
394 394 when 'attachment', 'relation'
395 395 l(:text_journal_added, :label => label, :value => value).html_safe
396 396 end
397 397 else
398 398 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
399 399 end
400 400 end
401 401
402 402 # Find the name of an associated record stored in the field attribute
403 403 def find_name_by_reflection(field, id)
404 404 unless id.present?
405 405 return nil
406 406 end
407 407 association = Issue.reflect_on_association(field.to_sym)
408 408 if association
409 409 record = association.class_name.constantize.find_by_id(id)
410 410 if record
411 411 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
412 412 return record.name
413 413 end
414 414 end
415 415 end
416 416
417 417 # Renders issue children recursively
418 418 def render_api_issue_children(issue, api)
419 419 return if issue.leaf?
420 420 api.array :children do
421 421 issue.children.each do |child|
422 422 api.issue(:id => child.id) do
423 423 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
424 424 api.subject child.subject
425 425 render_api_issue_children(child, api)
426 426 end
427 427 end
428 428 end
429 429 end
430 430 end
@@ -1,46 +1,46
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 def render_notes(issue, journal, options={})
22 22 content = ''
23 23 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)))
24 24 links = []
25 25 if !journal.notes.blank?
26 26 links << link_to(image_tag('comment.png'),
27 27 {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
28 28 :remote => true,
29 29 :method => 'post',
30 30 :title => l(:button_quote)) if options[:reply_links]
31 31 links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
32 32 { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
33 33 :title => l(:button_edit)) if editable
34 34 end
35 35 content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
36 36 content << textilizable(journal, :notes)
37 37 css_classes = "wiki"
38 38 css_classes << " editable" if editable
39 39 content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
40 40 end
41 41
42 42 def link_to_in_place_notes_editor(text, field_id, url, options={})
43 43 onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
44 44 link_to text, '#', options.merge(:onclick => onclick)
45 45 end
46 46 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,35 +1,35
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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)
22 22 scope = Principal.active.sorted.not_member_of(project).like(params[:q])
23 23 principal_count = scope.count
24 24 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
25 25 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
26 26
27 27 s = content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals')
28 28
29 29 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
30 30 link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
31 31 }
32 32
33 33 s + content_tag('p', links, :class => 'pagination')
34 34 end
35 35 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,71 +1,71
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 all
27 27 end
28 28
29 29 def documents_items
30 30 Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).all
31 31 end
32 32
33 33 def issuesassignedtome_items
34 34 Issue.visible.open.
35 35 where(:assigned_to_id => ([User.current.id] + User.current.group_ids)).
36 36 limit(10).
37 37 includes(:status, :project, :tracker, :priority).
38 38 order("#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC").
39 39 all
40 40 end
41 41
42 42 def issuesreportedbyme_items
43 43 Issue.visible.
44 44 where(:author_id => User.current.id).
45 45 limit(10).
46 46 includes(:status, :project, :tracker).
47 47 order("#{Issue.table_name}.updated_on DESC").
48 48 all
49 49 end
50 50
51 51 def issueswatched_items
52 52 Issue.visible.on_active_project.watched_by(User.current.id).recently_updated.limit(10).all
53 53 end
54 54
55 55 def news_items
56 56 News.visible.
57 57 where(:project_id => User.current.projects.map(&:id)).
58 58 limit(10).
59 59 includes(:project, :author).
60 60 order("#{News.table_name}.created_on DESC").
61 61 all
62 62 end
63 63
64 64 def timelog_items
65 65 TimeEntry.
66 66 where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, Date.today - 6, Date.today).
67 67 includes(:activity, :project, {:issue => [:tracker, :status]}).
68 68 order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC").
69 69 all
70 70 end
71 71 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,84 +1,84
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 link_to_version(version, options = {})
22 22 return '' unless version && version.is_a?(Version)
23 23 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
24 24 end
25 25
26 26 def project_settings_tabs
27 27 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
28 28 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
29 29 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
30 30 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
31 31 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
32 32 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
33 33 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
34 34 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
35 35 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
36 36 ]
37 37 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
38 38 end
39 39
40 40 def parent_project_select_tag(project)
41 41 selected = project.parent
42 42 # retrieve the requested parent project
43 43 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
44 44 if parent_id
45 45 selected = (parent_id.blank? ? nil : Project.find(parent_id))
46 46 end
47 47
48 48 options = ''
49 49 options << "<option value=''>&nbsp;</option>" if project.allowed_parents.include?(nil)
50 50 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
51 51 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
52 52 end
53 53
54 54 # Renders the projects index
55 55 def render_project_hierarchy(projects)
56 56 render_project_nested_lists(projects) do |project|
57 57 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
58 58 if project.description.present?
59 59 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
60 60 end
61 61 s
62 62 end
63 63 end
64 64
65 65 # Returns a set of options for a select field, grouped by project.
66 66 def version_options_for_select(versions, selected=nil)
67 67 grouped = Hash.new {|h,k| h[k] = []}
68 68 versions.each do |version|
69 69 grouped[version.project.name] << [version.name, version.id]
70 70 end
71 71
72 72 selected = selected.is_a?(Version) ? selected.id : selected
73 73 if grouped.keys.size > 1
74 74 grouped_options_for_select(grouped, selected)
75 75 else
76 76 options_for_select((grouped.values.first || []), selected)
77 77 end
78 78 end
79 79
80 80 def format_version_sharing(sharing)
81 81 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
82 82 l("label_version_sharing_#{sharing}")
83 83 end
84 84 end
@@ -1,198 +1,198
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 def filters_options_for_select(query)
22 22 options_for_select(filters_options(query))
23 23 end
24 24
25 25 def filters_options(query)
26 26 options = [[]]
27 27 options += query.available_filters.map do |field, field_options|
28 28 [field_options[:name], field]
29 29 end
30 30 end
31 31
32 32 def query_filters_hidden_tags(query)
33 33 tags = ''.html_safe
34 34 query.filters.each do |field, options|
35 35 tags << hidden_field_tag("f[]", field, :id => nil)
36 36 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
37 37 options[:values].each do |value|
38 38 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
39 39 end
40 40 end
41 41 tags
42 42 end
43 43
44 44 def query_columns_hidden_tags(query)
45 45 tags = ''.html_safe
46 46 query.columns.each do |column|
47 47 tags << hidden_field_tag("c[]", column.name, :id => nil)
48 48 end
49 49 tags
50 50 end
51 51
52 52 def query_hidden_tags(query)
53 53 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
54 54 end
55 55
56 56 def available_block_columns_tags(query)
57 57 tags = ''.html_safe
58 58 query.available_block_columns.each do |column|
59 59 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
60 60 end
61 61 tags
62 62 end
63 63
64 64 def query_available_inline_columns_options(query)
65 65 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
66 66 end
67 67
68 68 def query_selected_inline_columns_options(query)
69 69 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
70 70 end
71 71
72 72 def render_query_columns_selection(query, options={})
73 73 tag_name = (options[:name] || 'c') + '[]'
74 74 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
75 75 end
76 76
77 77 def column_header(column)
78 78 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
79 79 :default_order => column.default_order) :
80 80 content_tag('th', h(column.caption))
81 81 end
82 82
83 83 def column_content(column, issue)
84 84 value = column.value(issue)
85 85 if value.is_a?(Array)
86 86 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
87 87 else
88 88 column_value(column, issue, value)
89 89 end
90 90 end
91 91
92 92 def column_value(column, issue, value)
93 93 case column.name
94 94 when :id
95 95 link_to value, issue_path(issue)
96 96 when :subject
97 97 link_to value, issue_path(issue)
98 98 when :description
99 99 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
100 100 when :done_ratio
101 101 progress_bar(value, :width => '80px')
102 102 when :relations
103 103 other = value.other_issue(issue)
104 104 content_tag('span',
105 105 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
106 106 :class => value.css_classes_for(issue))
107 107 else
108 108 format_object(value)
109 109 end
110 110 end
111 111
112 112 def csv_content(column, issue)
113 113 value = column.value(issue)
114 114 if value.is_a?(Array)
115 115 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
116 116 else
117 117 csv_value(column, issue, value)
118 118 end
119 119 end
120 120
121 121 def csv_value(column, issue, value)
122 122 case value.class.name
123 123 when 'Time'
124 124 format_time(value)
125 125 when 'Date'
126 126 format_date(value)
127 127 when 'Float'
128 128 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
129 129 when 'IssueRelation'
130 130 other = value.other_issue(issue)
131 131 l(value.label_for(issue)) + " ##{other.id}"
132 132 else
133 133 value.to_s
134 134 end
135 135 end
136 136
137 137 def query_to_csv(items, query, options={})
138 138 encoding = l(:general_csv_encoding)
139 139 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
140 140 query.available_block_columns.each do |column|
141 141 if options[column.name].present?
142 142 columns << column
143 143 end
144 144 end
145 145
146 146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
147 147 # csv header fields
148 148 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
149 149 # csv lines
150 150 items.each do |item|
151 151 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
152 152 end
153 153 end
154 154 export
155 155 end
156 156
157 157 # Retrieve query from session or build a new query
158 158 def retrieve_query
159 159 if !params[:query_id].blank?
160 160 cond = "project_id IS NULL"
161 161 cond << " OR project_id = #{@project.id}" if @project
162 162 @query = IssueQuery.where(cond).find(params[:query_id])
163 163 raise ::Unauthorized unless @query.visible?
164 164 @query.project = @project
165 165 session[:query] = {:id => @query.id, :project_id => @query.project_id}
166 166 sort_clear
167 167 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
168 168 # Give it a name, required to be valid
169 169 @query = IssueQuery.new(:name => "_")
170 170 @query.project = @project
171 171 @query.build_from_params(params)
172 172 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
173 173 else
174 174 # retrieve from session
175 175 @query = nil
176 176 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
177 177 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
178 178 @query.project = @project
179 179 end
180 180 end
181 181
182 182 def retrieve_query_from_session
183 183 if session[:query]
184 184 if session[:query][:id]
185 185 @query = IssueQuery.find_by_id(session[:query][:id])
186 186 return unless @query
187 187 else
188 188 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
189 189 end
190 190 if session[:query].has_key?(:project_id)
191 191 @query.project_id = session[:query][:project_id]
192 192 else
193 193 @query.project = @project
194 194 end
195 195 @query
196 196 end
197 197 end
198 198 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,297 +1,297
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 without_leading_slash(path)
149 149 path.gsub(%r{^/+}, '')
150 150 end
151 151
152 152 def subversion_field_tags(form, repository)
153 153 content_tag('p', form.text_field(:url, :size => 60, :required => true,
154 154 :disabled => !repository.safe_attribute?('url')) +
155 155 '<br />'.html_safe +
156 156 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
157 157 content_tag('p', form.text_field(:login, :size => 30)) +
158 158 content_tag('p', form.password_field(
159 159 :password, :size => 30, :name => 'ignore',
160 160 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
161 161 :onfocus => "this.value=''; this.name='repository[password]';",
162 162 :onchange => "this.name='repository[password]';"))
163 163 end
164 164
165 165 def darcs_field_tags(form, repository)
166 166 content_tag('p', form.text_field(
167 167 :url, :label => l(:field_path_to_repository),
168 168 :size => 60, :required => true,
169 169 :disabled => !repository.safe_attribute?('url'))) +
170 170 content_tag('p', form.select(
171 171 :log_encoding, [nil] + Setting::ENCODINGS,
172 172 :label => l(:field_commit_logs_encoding), :required => true))
173 173 end
174 174
175 175 def mercurial_field_tags(form, repository)
176 176 content_tag('p', form.text_field(
177 177 :url, :label => l(:field_path_to_repository),
178 178 :size => 60, :required => true,
179 179 :disabled => !repository.safe_attribute?('url')
180 180 ) +
181 181 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
182 182 content_tag('p', form.select(
183 183 :path_encoding, [nil] + Setting::ENCODINGS,
184 184 :label => l(:field_scm_path_encoding)
185 185 ) +
186 186 '<br />'.html_safe + l(:text_scm_path_encoding_note))
187 187 end
188 188
189 189 def git_field_tags(form, repository)
190 190 content_tag('p', form.text_field(
191 191 :url, :label => l(:field_path_to_repository),
192 192 :size => 60, :required => true,
193 193 :disabled => !repository.safe_attribute?('url')
194 194 ) +
195 195 '<br />'.html_safe +
196 196 l(:text_git_repository_note)) +
197 197 content_tag('p', form.select(
198 198 :path_encoding, [nil] + Setting::ENCODINGS,
199 199 :label => l(:field_scm_path_encoding)
200 200 ) +
201 201 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
202 202 content_tag('p', form.check_box(
203 203 :extra_report_last_commit,
204 204 :label => l(:label_git_report_last_commit)
205 205 ))
206 206 end
207 207
208 208 def cvs_field_tags(form, repository)
209 209 content_tag('p', form.text_field(
210 210 :root_url,
211 211 :label => l(:field_cvsroot),
212 212 :size => 60, :required => true,
213 213 :disabled => !repository.safe_attribute?('root_url'))) +
214 214 content_tag('p', form.text_field(
215 215 :url,
216 216 :label => l(:field_cvs_module),
217 217 :size => 30, :required => true,
218 218 :disabled => !repository.safe_attribute?('url'))) +
219 219 content_tag('p', form.select(
220 220 :log_encoding, [nil] + Setting::ENCODINGS,
221 221 :label => l(:field_commit_logs_encoding), :required => true)) +
222 222 content_tag('p', form.select(
223 223 :path_encoding, [nil] + Setting::ENCODINGS,
224 224 :label => l(:field_scm_path_encoding)
225 225 ) +
226 226 '<br />'.html_safe + l(:text_scm_path_encoding_note))
227 227 end
228 228
229 229 def bazaar_field_tags(form, repository)
230 230 content_tag('p', form.text_field(
231 231 :url, :label => l(:field_path_to_repository),
232 232 :size => 60, :required => true,
233 233 :disabled => !repository.safe_attribute?('url'))) +
234 234 content_tag('p', form.select(
235 235 :log_encoding, [nil] + Setting::ENCODINGS,
236 236 :label => l(:field_commit_logs_encoding), :required => true))
237 237 end
238 238
239 239 def filesystem_field_tags(form, repository)
240 240 content_tag('p', form.text_field(
241 241 :url, :label => l(:field_root_directory),
242 242 :size => 60, :required => true,
243 243 :disabled => !repository.safe_attribute?('url'))) +
244 244 content_tag('p', form.select(
245 245 :path_encoding, [nil] + Setting::ENCODINGS,
246 246 :label => l(:field_scm_path_encoding)
247 247 ) +
248 248 '<br />'.html_safe + l(:text_scm_path_encoding_note))
249 249 end
250 250
251 251 def index_commits(commits, heads)
252 252 return nil if commits.nil? or commits.first.parents.nil?
253 253 refs_map = {}
254 254 heads.each do |head|
255 255 refs_map[head.scmid] ||= []
256 256 refs_map[head.scmid] << head
257 257 end
258 258 commits_by_scmid = {}
259 259 commits.reverse.each_with_index do |commit, commit_index|
260 260 commits_by_scmid[commit.scmid] = {
261 261 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
262 262 :rdmid => commit_index,
263 263 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
264 264 :scmid => commit.scmid,
265 265 :href => block_given? ? yield(commit.scmid) : commit.scmid
266 266 }
267 267 end
268 268 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
269 269 space = nil
270 270 heads.each do |head|
271 271 if commits_by_scmid.include? head.scmid
272 272 space = index_head((space || -1) + 1, head, commits_by_scmid)
273 273 end
274 274 end
275 275 # when no head matched anything use first commit
276 276 space ||= index_head(0, commits.first, commits_by_scmid)
277 277 return commits_by_scmid, space
278 278 end
279 279
280 280 def index_head(space, commit, commits_by_scmid)
281 281 stack = [[space, commits_by_scmid[commit.scmid]]]
282 282 max_space = space
283 283 until stack.empty?
284 284 space, commit = stack.pop
285 285 commit[:space] = space if commit[:space].nil?
286 286 space -= 1
287 287 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
288 288 parent_commit = commits_by_scmid[parent_scmid]
289 289 if parent_commit and parent_commit[:space].nil?
290 290 stack.unshift [space += 1, parent_commit]
291 291 end
292 292 end
293 293 max_space = space if max_space < space
294 294 end
295 295 max_space
296 296 end
297 297 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,39 +1,39
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 end
@@ -1,70 +1,70
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 words = words.mb_chars
33 33 if i.even?
34 34 result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
35 35 else
36 36 t = (tokens.index(words.downcase) || 0) % 4
37 37 result << content_tag('span', h(words), :class => "highlight token-#{t}")
38 38 end
39 39 end
40 40 result.html_safe
41 41 end
42 42
43 43 def type_label(t)
44 44 l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
45 45 end
46 46
47 47 def project_select_tag
48 48 options = [[l(:label_project_all), 'all']]
49 49 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
50 50 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
51 51 options << [@project.name, ''] unless @project.nil?
52 52 label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
53 53 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
54 54 end
55 55
56 56 def render_results_by_type(results_by_type)
57 57 links = []
58 58 # Sorts types by results count
59 59 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
60 60 c = results_by_type[t]
61 61 next if c == 0
62 62 text = "#{type_label(t)} (#{c})"
63 63 links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
64 64 :all_words => params[:all_words], :scope => params[:scope], t => 1)
65 65 end
66 66 ('<ul>'.html_safe +
67 67 links.map {|link| content_tag('li', link)}.join(' ').html_safe +
68 68 '</ul>'.html_safe) unless links.empty?
69 69 end
70 70 end
@@ -1,106 +1,106
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
26 26 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
27 27 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
28 28 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
29 29 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
30 30 ]
31 31 end
32 32
33 33 def setting_select(setting, choices, options={})
34 34 if blank_text = options.delete(:blank)
35 35 choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
36 36 end
37 37 setting_label(setting, options).html_safe +
38 38 select_tag("settings[#{setting}]",
39 39 options_for_select(choices, Setting.send(setting).to_s),
40 40 options).html_safe
41 41 end
42 42
43 43 def setting_multiselect(setting, choices, options={})
44 44 setting_values = Setting.send(setting)
45 45 setting_values = [] unless setting_values.is_a?(Array)
46 46
47 47 content_tag("label", l(options[:label] || "setting_#{setting}")) +
48 48 hidden_field_tag("settings[#{setting}][]", '').html_safe +
49 49 choices.collect do |choice|
50 50 text, value = (choice.is_a?(Array) ? choice : [choice, choice])
51 51 content_tag(
52 52 'label',
53 53 check_box_tag(
54 54 "settings[#{setting}][]",
55 55 value,
56 56 setting_values.include?(value),
57 57 :id => nil
58 58 ) + text.to_s,
59 59 :class => (options[:inline] ? 'inline' : 'block')
60 60 )
61 61 end.join.html_safe
62 62 end
63 63
64 64 def setting_text_field(setting, options={})
65 65 setting_label(setting, options).html_safe +
66 66 text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
67 67 end
68 68
69 69 def setting_text_area(setting, options={})
70 70 setting_label(setting, options).html_safe +
71 71 text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
72 72 end
73 73
74 74 def setting_check_box(setting, options={})
75 75 setting_label(setting, options).html_safe +
76 76 hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
77 77 check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
78 78 end
79 79
80 80 def setting_label(setting, options={})
81 81 label = options.delete(:label)
82 82 label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}"), options[:label_options]).html_safe : ''
83 83 end
84 84
85 85 # Renders a notification field for a Redmine::Notifiable option
86 86 def notification_field(notifiable)
87 87 return content_tag(:label,
88 88 check_box_tag('settings[notified_events][]',
89 89 notifiable.name,
90 90 Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
91 91 l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
92 92 :class => notifiable.parent.present? ? "parent" : '').html_safe
93 93 end
94 94
95 95 def cross_project_subtasks_options
96 96 options = [
97 97 [:label_disabled, ''],
98 98 [:label_cross_project_system, 'system'],
99 99 [:label_cross_project_tree, 'tree'],
100 100 [:label_cross_project_hierarchy, 'hierarchy'],
101 101 [:label_cross_project_descendants, 'descendants']
102 102 ]
103 103
104 104 options.map {|label, value| [l(label), value.to_s]}
105 105 end
106 106 end
@@ -1,156 +1,156
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 ||= @project
42 42 if project.nil?
43 43 activities = TimeEntryActivity.shared.active
44 44 else
45 45 activities = project.activities
46 46 end
47 47
48 48 collection = []
49 49 if time_entry && time_entry.activity && !time_entry.activity.active?
50 50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
51 51 else
52 52 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
53 53 end
54 54 activities.each { |a| collection << [a.name, a.id] }
55 55 collection
56 56 end
57 57
58 58 def select_hours(data, criteria, value)
59 59 if value.to_s.empty?
60 60 data.select {|row| row[criteria].blank? }
61 61 else
62 62 data.select {|row| row[criteria].to_s == value.to_s}
63 63 end
64 64 end
65 65
66 66 def sum_hours(data)
67 67 sum = 0
68 68 data.each do |row|
69 69 sum += row['hours'].to_f
70 70 end
71 71 sum
72 72 end
73 73
74 74 def options_for_period_select(value)
75 75 options_for_select([[l(:label_all_time), 'all'],
76 76 [l(:label_today), 'today'],
77 77 [l(:label_yesterday), 'yesterday'],
78 78 [l(:label_this_week), 'current_week'],
79 79 [l(:label_last_week), 'last_week'],
80 80 [l(:label_last_n_weeks, 2), 'last_2_weeks'],
81 81 [l(:label_last_n_days, 7), '7_days'],
82 82 [l(:label_this_month), 'current_month'],
83 83 [l(:label_last_month), 'last_month'],
84 84 [l(:label_last_n_days, 30), '30_days'],
85 85 [l(:label_this_year), 'current_year']],
86 86 value)
87 87 end
88 88
89 89 def format_criteria_value(criteria_options, value)
90 90 if value.blank?
91 91 "[#{l(:label_none)}]"
92 92 elsif k = criteria_options[:klass]
93 93 obj = k.find_by_id(value.to_i)
94 94 if obj.is_a?(Issue)
95 95 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
96 96 else
97 97 obj
98 98 end
99 99 elsif cf = criteria_options[:custom_field]
100 100 format_value(value, cf)
101 101 else
102 102 value.to_s
103 103 end
104 104 end
105 105
106 106 def report_to_csv(report)
107 107 decimal_separator = l(:general_csv_decimal_separator)
108 108 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
109 109 # Column headers
110 110 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
111 111 headers += report.periods
112 112 headers << l(:label_total_time)
113 113 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
114 114 c.to_s,
115 115 l(:general_csv_encoding) ) }
116 116 # Content
117 117 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
118 118 # Total row
119 119 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total_time), l(:general_csv_encoding))
120 120 row = [ str_total ] + [''] * (report.criteria.size - 1)
121 121 total = 0
122 122 report.periods.each do |period|
123 123 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
124 124 total += sum
125 125 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
126 126 end
127 127 row << ("%.2f" % total).gsub('.',decimal_separator)
128 128 csv << row
129 129 end
130 130 export
131 131 end
132 132
133 133 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
134 134 decimal_separator = l(:general_csv_decimal_separator)
135 135 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
136 136 hours_for_value = select_hours(hours, criteria[level], value)
137 137 next if hours_for_value.empty?
138 138 row = [''] * level
139 139 row << Redmine::CodesetUtil.from_utf8(
140 140 format_criteria_value(available_criteria[criteria[level]], value).to_s,
141 141 l(:general_csv_encoding) )
142 142 row += [''] * (criteria.length - level - 1)
143 143 total = 0
144 144 periods.each do |period|
145 145 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
146 146 total += sum
147 147 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
148 148 end
149 149 row << ("%.2f" % total).gsub('.',decimal_separator)
150 150 csv << row
151 151 if criteria.length > level + 1
152 152 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
153 153 end
154 154 end
155 155 end
156 156 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,54 +1,54
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 user_settings_tabs
46 46 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
47 47 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
48 48 ]
49 49 if Group.all.any?
50 50 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
51 51 end
52 52 tabs
53 53 end
54 54 end
@@ -1,54 +1,54
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
31 31
32 32 def render_issue_status_by(version, criteria)
33 33 criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
34 34
35 35 h = Hash.new {|k,v| k[v] = [0, 0]}
36 36 begin
37 37 # Total issue count
38 38 Issue.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][0] = s}
39 39 # Open issues count
40 40 Issue.open.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][1] = s}
41 41 rescue ActiveRecord::RecordNotFound
42 42 # When grouping by an association, Rails throws this exception if there's no result (bug)
43 43 end
44 44 # Sort with nil keys in last position
45 45 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])}}
46 46 max = counts.collect {|c| c[:total]}.max
47 47
48 48 render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
49 49 end
50 50
51 51 def status_by_options_for_select(value)
52 52 options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
53 53 end
54 54 end
@@ -1,82 +1,82
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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_tag(object, user, options={})
23 23 ActiveSupport::Deprecation.warn "#watcher_tag is deprecated and will be removed in Redmine 3.0. Use #watcher_link instead."
24 24 watcher_link(object, user)
25 25 end
26 26
27 27 def watcher_link(objects, user)
28 28 return '' unless user && user.logged?
29 29 objects = Array.wrap(objects)
30 30
31 31 watched = Watcher.any_watched?(objects, user)
32 32 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
33 33 text = watched ? l(:button_unwatch) : l(:button_watch)
34 34 url = watch_path(
35 35 :object_type => objects.first.class.to_s.underscore,
36 36 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
37 37 )
38 38 method = watched ? 'delete' : 'post'
39 39
40 40 link_to text, url, :remote => true, :method => method, :class => css
41 41 end
42 42
43 43 # Returns the css class used to identify watch links for a given +object+
44 44 def watcher_css(objects)
45 45 objects = Array.wrap(objects)
46 46 id = (objects.size == 1 ? objects.first.id : 'bulk')
47 47 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
48 48 end
49 49
50 50 # Returns a comma separated list of users watching the given object
51 51 def watchers_list(object)
52 52 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
53 53 content = ''.html_safe
54 54 lis = object.watcher_users.collect do |user|
55 55 s = ''.html_safe
56 56 s << avatar(user, :size => "16").to_s
57 57 s << link_to_user(user, :class => 'user')
58 58 if remove_allowed
59 59 url = {:controller => 'watchers',
60 60 :action => 'destroy',
61 61 :object_type => object.class.to_s.underscore,
62 62 :object_id => object.id,
63 63 :user_id => user}
64 64 s << ' '
65 65 s << link_to(image_tag('delete.png'), url,
66 66 :remote => true, :method => 'delete', :class => "delete")
67 67 end
68 68 content << content_tag('li', s, :class => "user-#{user.id}")
69 69 end
70 70 content.present? ? content_tag('ul', content, :class => 'watchers') : content
71 71 end
72 72
73 73 def watchers_checkboxes(object, users, checked=nil)
74 74 users.map do |user|
75 75 c = checked.nil? ? object.watched_by?(user) : checked
76 76 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
77 77 content_tag 'label', "#{tag} #{h(user)}".html_safe,
78 78 :id => "issue_watcher_user_ids_#{user.id}",
79 79 :class => "floating"
80 80 end.join.html_safe
81 81 end
82 82 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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
22 22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 24 s = ''.html_safe
25 25 if pages.has_key?(parent)
26 26 pages[parent].each do |page|
27 27 attrs = "value='#{page.id}'"
28 28 attrs << " selected='selected'" if selected == page
29 29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30 30
31 31 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 33 end
34 34 end
35 35 s
36 36 end
37 37
38 38 def wiki_page_breadcrumb(page)
39 39 breadcrumb(page.ancestors.reverse.collect {|parent|
40 40 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
41 41 })
42 42 end
43 43 end
@@ -1,41 +1,41
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 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 field_required?(field)
22 22 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
23 23 end
24 24
25 25 def field_permission_tag(permissions, status, field, role)
26 26 name = field.is_a?(CustomField) ? field.id.to_s : field
27 27 options = [["", ""], [l(:label_readonly), "readonly"]]
28 28 options << [l(:label_required), "required"] unless field_required?(field)
29 29 html_options = {}
30 30 selected = permissions[status.id][name]
31 31
32 32 hidden = field.is_a?(CustomField) && !field.visible? && !role.custom_fields.to_a.include?(field)
33 33 if hidden
34 34 options[0][0] = l(:label_hidden)
35 35 selected = ''
36 36 html_options[:disabled] = true
37 37 end
38 38
39 39 select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, selected), html_options)
40 40 end
41 41 end
@@ -1,337 +1,337
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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", :foreign_key => "author_id"
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
30 30
31 31 acts_as_event :title => :filename,
32 32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
33 33
34 34 acts_as_activity_provider :type => 'files',
35 35 :permission => :view_files,
36 36 :author_key => :author_id,
37 37 :find_options => {:select => "#{Attachment.table_name}.*",
38 38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
39 39 "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 )"}
40 40
41 41 acts_as_activity_provider :type => 'documents',
42 42 :permission => :view_documents,
43 43 :author_key => :author_id,
44 44 :find_options => {:select => "#{Attachment.table_name}.*",
45 45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
46 46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
47 47
48 48 cattr_accessor :storage_path
49 49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
50 50
51 51 cattr_accessor :thumbnails_storage_path
52 52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
53 53
54 54 before_save :files_to_final_location
55 55 after_destroy :delete_from_disk
56 56
57 57 # Returns an unsaved copy of the attachment
58 58 def copy(attributes=nil)
59 59 copy = self.class.new
60 60 copy.attributes = self.attributes.dup.except("id", "downloads")
61 61 copy.attributes = attributes if attributes
62 62 copy
63 63 end
64 64
65 65 def validate_max_file_size
66 66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
67 67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
68 68 end
69 69 end
70 70
71 71 def file=(incoming_file)
72 72 unless incoming_file.nil?
73 73 @temp_file = incoming_file
74 74 if @temp_file.size > 0
75 75 if @temp_file.respond_to?(:original_filename)
76 76 self.filename = @temp_file.original_filename
77 77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
78 78 end
79 79 if @temp_file.respond_to?(:content_type)
80 80 self.content_type = @temp_file.content_type.to_s.chomp
81 81 end
82 82 if content_type.blank? && filename.present?
83 83 self.content_type = Redmine::MimeType.of(filename)
84 84 end
85 85 self.filesize = @temp_file.size
86 86 end
87 87 end
88 88 end
89 89
90 90 def file
91 91 nil
92 92 end
93 93
94 94 def filename=(arg)
95 95 write_attribute :filename, sanitize_filename(arg.to_s)
96 96 filename
97 97 end
98 98
99 99 # Copies the temporary file to its final location
100 100 # and computes its MD5 hash
101 101 def files_to_final_location
102 102 if @temp_file && (@temp_file.size > 0)
103 103 self.disk_directory = target_directory
104 104 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
105 105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
106 106 path = File.dirname(diskfile)
107 107 unless File.directory?(path)
108 108 FileUtils.mkdir_p(path)
109 109 end
110 110 md5 = Digest::MD5.new
111 111 File.open(diskfile, "wb") do |f|
112 112 if @temp_file.respond_to?(:read)
113 113 buffer = ""
114 114 while (buffer = @temp_file.read(8192))
115 115 f.write(buffer)
116 116 md5.update(buffer)
117 117 end
118 118 else
119 119 f.write(@temp_file)
120 120 md5.update(@temp_file)
121 121 end
122 122 end
123 123 self.digest = md5.hexdigest
124 124 end
125 125 @temp_file = nil
126 126 # Don't save the content type if it's longer than the authorized length
127 127 if self.content_type && self.content_type.length > 255
128 128 self.content_type = nil
129 129 end
130 130 end
131 131
132 132 # Deletes the file from the file system if it's not referenced by other attachments
133 133 def delete_from_disk
134 134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
135 135 delete_from_disk!
136 136 end
137 137 end
138 138
139 139 # Returns file's location on disk
140 140 def diskfile
141 141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
142 142 end
143 143
144 144 def title
145 145 title = filename.to_s
146 146 if description.present?
147 147 title << " (#{description})"
148 148 end
149 149 title
150 150 end
151 151
152 152 def increment_download
153 153 increment!(:downloads)
154 154 end
155 155
156 156 def project
157 157 container.try(:project)
158 158 end
159 159
160 160 def visible?(user=User.current)
161 161 if container_id
162 162 container && container.attachments_visible?(user)
163 163 else
164 164 author == user
165 165 end
166 166 end
167 167
168 168 def deletable?(user=User.current)
169 169 if container_id
170 170 container && container.attachments_deletable?(user)
171 171 else
172 172 author == user
173 173 end
174 174 end
175 175
176 176 def image?
177 177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
178 178 end
179 179
180 180 def thumbnailable?
181 181 image?
182 182 end
183 183
184 184 # Returns the full path the attachment thumbnail, or nil
185 185 # if the thumbnail cannot be generated.
186 186 def thumbnail(options={})
187 187 if thumbnailable? && readable?
188 188 size = options[:size].to_i
189 189 if size > 0
190 190 # Limit the number of thumbnails per image
191 191 size = (size / 50) * 50
192 192 # Maximum thumbnail size
193 193 size = 800 if size > 800
194 194 else
195 195 size = Setting.thumbnails_size.to_i
196 196 end
197 197 size = 100 unless size > 0
198 198 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
199 199
200 200 begin
201 201 Redmine::Thumbnail.generate(self.diskfile, target, size)
202 202 rescue => e
203 203 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
204 204 return nil
205 205 end
206 206 end
207 207 end
208 208
209 209 # Deletes all thumbnails
210 210 def self.clear_thumbnails
211 211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
212 212 File.delete file
213 213 end
214 214 end
215 215
216 216 def is_text?
217 217 Redmine::MimeType.is_type?('text', filename)
218 218 end
219 219
220 220 def is_diff?
221 221 self.filename =~ /\.(patch|diff)$/i
222 222 end
223 223
224 224 # Returns true if the file is readable
225 225 def readable?
226 226 File.readable?(diskfile)
227 227 end
228 228
229 229 # Returns the attachment token
230 230 def token
231 231 "#{id}.#{digest}"
232 232 end
233 233
234 234 # Finds an attachment that matches the given token and that has no container
235 235 def self.find_by_token(token)
236 236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
237 237 attachment_id, attachment_digest = $1, $2
238 238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
239 239 if attachment && attachment.container.nil?
240 240 attachment
241 241 end
242 242 end
243 243 end
244 244
245 245 # Bulk attaches a set of files to an object
246 246 #
247 247 # Returns a Hash of the results:
248 248 # :files => array of the attached files
249 249 # :unsaved => array of the files that could not be attached
250 250 def self.attach_files(obj, attachments)
251 251 result = obj.save_attachments(attachments, User.current)
252 252 obj.attach_saved_attachments
253 253 result
254 254 end
255 255
256 256 def self.latest_attach(attachments, filename)
257 257 attachments.sort_by(&:created_on).reverse.detect {
258 258 |att| att.filename.downcase == filename.downcase
259 259 }
260 260 end
261 261
262 262 def self.prune(age=1.day)
263 263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
264 264 end
265 265
266 266 # Moves an existing attachment to its target directory
267 267 def move_to_target_directory!
268 268 return unless !new_record? & readable?
269 269
270 270 src = diskfile
271 271 self.disk_directory = target_directory
272 272 dest = diskfile
273 273
274 274 return if src == dest
275 275
276 276 if !FileUtils.mkdir_p(File.dirname(dest))
277 277 logger.error "Could not create directory #{File.dirname(dest)}" if logger
278 278 return
279 279 end
280 280
281 281 if !FileUtils.mv(src, dest)
282 282 logger.error "Could not move attachment from #{src} to #{dest}" if logger
283 283 return
284 284 end
285 285
286 286 update_column :disk_directory, disk_directory
287 287 end
288 288
289 289 # Moves existing attachments that are stored at the root of the files
290 290 # directory (ie. created before Redmine 2.3) to their target subdirectories
291 291 def self.move_from_root_to_target_directory
292 292 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
293 293 attachment.move_to_target_directory!
294 294 end
295 295 end
296 296
297 297 private
298 298
299 299 # Physically deletes the file from the file system
300 300 def delete_from_disk!
301 301 if disk_filename.present? && File.exist?(diskfile)
302 302 File.delete(diskfile)
303 303 end
304 304 end
305 305
306 306 def sanitize_filename(value)
307 307 # get only the filename, not the whole path
308 308 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
309 309
310 310 # Finally, replace invalid characters with underscore
311 311 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
312 312 end
313 313
314 314 # Returns the subdirectory in which the attachment will be saved
315 315 def target_directory
316 316 time = created_on || DateTime.now
317 317 time.strftime("%Y/%m")
318 318 end
319 319
320 320 # Returns an ASCII or hashed filename that do not
321 321 # exists yet in the given subdirectory
322 322 def self.disk_filename(filename, directory=nil)
323 323 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
324 324 ascii = ''
325 325 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
326 326 ascii = filename
327 327 else
328 328 ascii = Digest::MD5.hexdigest(filename)
329 329 # keep the extension if any
330 330 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
331 331 end
332 332 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
333 333 timestamp.succ!
334 334 end
335 335 "#{timestamp}_#{ascii}"
336 336 end
337 337 end
@@ -1,92 +1,92
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
33 33 def authenticate(login, password)
34 34 end
35 35
36 36 def test_connection
37 37 end
38 38
39 39 def auth_method_name
40 40 "Abstract"
41 41 end
42 42
43 43 def account_password
44 44 read_ciphered_attribute(:account_password)
45 45 end
46 46
47 47 def account_password=(arg)
48 48 write_ciphered_attribute(:account_password, arg)
49 49 end
50 50
51 51 def searchable?
52 52 false
53 53 end
54 54
55 55 def self.search(q)
56 56 results = []
57 57 AuthSource.all.each do |source|
58 58 begin
59 59 if source.searchable?
60 60 results += source.search(q)
61 61 end
62 62 rescue AuthSourceException => e
63 63 logger.error "Error while searching users in #{source.name}: #{e.message}"
64 64 end
65 65 end
66 66 results
67 67 end
68 68
69 69 def allow_password_changes?
70 70 self.class.allow_password_changes?
71 71 end
72 72
73 73 # Does this auth source backend allow password changes?
74 74 def self.allow_password_changes?
75 75 false
76 76 end
77 77
78 78 # Try to authenticate a user not yet registered against available sources
79 79 def self.authenticate(login, password)
80 80 AuthSource.where(:onthefly_register => true).each do |source|
81 81 begin
82 82 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
83 83 attrs = source.authenticate(login, password)
84 84 rescue => e
85 85 logger.error "Error during authentication: #{e.message}"
86 86 attrs = nil
87 87 end
88 88 return attrs if attrs
89 89 end
90 90 return nil
91 91 end
92 92 end
@@ -1,200 +1,200
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 validates_presence_of :host, :port, :attr_login
24 24 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
25 25 validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
26 26 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
27 27 validates_numericality_of :port, :only_integer => true
28 28 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
29 29 validate :validate_filter
30 30
31 31 before_validation :strip_ldap_attributes
32 32
33 33 def initialize(attributes=nil, *args)
34 34 super
35 35 self.port = 389 if self.port == 0
36 36 end
37 37
38 38 def authenticate(login, password)
39 39 return nil if login.blank? || password.blank?
40 40
41 41 with_timeout do
42 42 attrs = get_user_dn(login, password)
43 43 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
44 44 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
45 45 return attrs.except(:dn)
46 46 end
47 47 end
48 48 rescue Net::LDAP::LdapError => e
49 49 raise AuthSourceException.new(e.message)
50 50 end
51 51
52 52 # test the connection to the LDAP
53 53 def test_connection
54 54 with_timeout do
55 55 ldap_con = initialize_ldap_con(self.account, self.account_password)
56 56 ldap_con.open { }
57 57 end
58 58 rescue Net::LDAP::LdapError => e
59 59 raise AuthSourceException.new(e.message)
60 60 end
61 61
62 62 def auth_method_name
63 63 "LDAP"
64 64 end
65 65
66 66 # Returns true if this source can be searched for users
67 67 def searchable?
68 68 !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
69 69 end
70 70
71 71 # Searches the source for users and returns an array of results
72 72 def search(q)
73 73 q = q.to_s.strip
74 74 return [] unless searchable? && q.present?
75 75
76 76 results = []
77 77 search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
78 78 ldap_con = initialize_ldap_con(self.account, self.account_password)
79 79 ldap_con.search(:base => self.base_dn,
80 80 :filter => search_filter,
81 81 :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
82 82 :size => 10) do |entry|
83 83 attrs = get_user_attributes_from_ldap_entry(entry)
84 84 attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
85 85 results << attrs
86 86 end
87 87 results
88 88 rescue Net::LDAP::LdapError => e
89 89 raise AuthSourceException.new(e.message)
90 90 end
91 91
92 92 private
93 93
94 94 def with_timeout(&block)
95 95 timeout = self.timeout
96 96 timeout = 20 unless timeout && timeout > 0
97 97 Timeout.timeout(timeout) do
98 98 return yield
99 99 end
100 100 rescue Timeout::Error => e
101 101 raise AuthSourceTimeoutException.new(e.message)
102 102 end
103 103
104 104 def ldap_filter
105 105 if filter.present?
106 106 Net::LDAP::Filter.construct(filter)
107 107 end
108 108 rescue Net::LDAP::LdapError
109 109 nil
110 110 end
111 111
112 112 def base_filter
113 113 filter = Net::LDAP::Filter.eq("objectClass", "*")
114 114 if f = ldap_filter
115 115 filter = filter & f
116 116 end
117 117 filter
118 118 end
119 119
120 120 def validate_filter
121 121 if filter.present? && ldap_filter.nil?
122 122 errors.add(:filter, :invalid)
123 123 end
124 124 end
125 125
126 126 def strip_ldap_attributes
127 127 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
128 128 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
129 129 end
130 130 end
131 131
132 132 def initialize_ldap_con(ldap_user, ldap_password)
133 133 options = { :host => self.host,
134 134 :port => self.port,
135 135 :encryption => (self.tls ? :simple_tls : nil)
136 136 }
137 137 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
138 138 Net::LDAP.new options
139 139 end
140 140
141 141 def get_user_attributes_from_ldap_entry(entry)
142 142 {
143 143 :dn => entry.dn,
144 144 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
145 145 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
146 146 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
147 147 :auth_source_id => self.id
148 148 }
149 149 end
150 150
151 151 # Return the attributes needed for the LDAP search. It will only
152 152 # include the user attributes if on-the-fly registration is enabled
153 153 def search_attributes
154 154 if onthefly_register?
155 155 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
156 156 else
157 157 ['dn']
158 158 end
159 159 end
160 160
161 161 # Check if a DN (user record) authenticates with the password
162 162 def authenticate_dn(dn, password)
163 163 if dn.present? && password.present?
164 164 initialize_ldap_con(dn, password).bind
165 165 end
166 166 end
167 167
168 168 # Get the user's dn and any attributes for them, given their login
169 169 def get_user_dn(login, password)
170 170 ldap_con = nil
171 171 if self.account && self.account.include?("$login")
172 172 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
173 173 else
174 174 ldap_con = initialize_ldap_con(self.account, self.account_password)
175 175 end
176 176 attrs = {}
177 177 search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
178 178
179 179 ldap_con.search( :base => self.base_dn,
180 180 :filter => search_filter,
181 181 :attributes=> search_attributes) do |entry|
182 182
183 183 if onthefly_register?
184 184 attrs = get_user_attributes_from_ldap_entry(entry)
185 185 else
186 186 attrs = {:dn => entry.dn}
187 187 end
188 188
189 189 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
190 190 end
191 191
192 192 attrs
193 193 end
194 194
195 195 def self.get_attr(entry, attr_name)
196 196 if !attr_name.blank?
197 197 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
198 198 end
199 199 end
200 200 end
@@ -1,90 +1,90
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
22 22 has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
23 23 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
24 24 acts_as_tree :dependent => :nullify
25 25 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
26 26 acts_as_watchable
27 27
28 28 validates_presence_of :name, :description
29 29 validates_length_of :name, :maximum => 30
30 30 validates_length_of :description, :maximum => 255
31 31 validate :validate_board
32 32
33 33 scope :visible, lambda {|*args|
34 34 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
35 35 }
36 36
37 37 safe_attributes 'name', 'description', 'parent_id', 'move_to'
38 38
39 39 def visible?(user=User.current)
40 40 !user.nil? && user.allowed_to?(:view_messages, project)
41 41 end
42 42
43 43 def reload(*args)
44 44 @valid_parents = nil
45 45 super
46 46 end
47 47
48 48 def to_s
49 49 name
50 50 end
51 51
52 52 def valid_parents
53 53 @valid_parents ||= project.boards - self_and_descendants
54 54 end
55 55
56 56 def reset_counters!
57 57 self.class.reset_counters!(id)
58 58 end
59 59
60 60 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
61 61 def self.reset_counters!(board_id)
62 62 board_id = board_id.to_i
63 63 where(["id = ?", board_id]).
64 64 update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
65 65 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
66 66 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})")
67 67 end
68 68
69 69 def self.board_tree(boards, parent_id=nil, level=0)
70 70 tree = []
71 71 boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
72 72 tree << [board, level]
73 73 tree += board_tree(boards, board.id, level+1)
74 74 end
75 75 if block_given?
76 76 tree.each do |board, level|
77 77 yield board, level
78 78 end
79 79 end
80 80 tree
81 81 end
82 82
83 83 protected
84 84
85 85 def validate_board
86 86 if parent_id && parent_id_changed?
87 87 errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
88 88 end
89 89 end
90 90 end
@@ -1,37 +1,37
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
25 25 def relative_path
26 26 changeset.repository.relative_path(path)
27 27 end
28 28
29 29 def replace_invalid_utf8_of_path
30 30 self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
31 31 self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
32 32 end
33 33
34 34 def init_path
35 35 self.path ||= ""
36 36 end
37 37 end
@@ -1,283 +1,283
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :include => {: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 :find_options => {:include => [: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
50 50 scope :visible, lambda {|*args|
51 51 includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
52 52 }
53 53
54 54 after_create :scan_for_issues
55 55 before_create :before_create_cs
56 56
57 57 def revision=(r)
58 58 write_attribute :revision, (r.nil? ? nil : r.to_s)
59 59 end
60 60
61 61 # Returns the identifier of this changeset; depending on repository backends
62 62 def identifier
63 63 if repository.class.respond_to? :changeset_identifier
64 64 repository.class.changeset_identifier self
65 65 else
66 66 revision.to_s
67 67 end
68 68 end
69 69
70 70 def committed_on=(date)
71 71 self.commit_date = date
72 72 super
73 73 end
74 74
75 75 # Returns the readable identifier
76 76 def format_identifier
77 77 if repository.class.respond_to? :format_changeset_identifier
78 78 repository.class.format_changeset_identifier self
79 79 else
80 80 identifier
81 81 end
82 82 end
83 83
84 84 def project
85 85 repository.project
86 86 end
87 87
88 88 def author
89 89 user || committer.to_s.split('<').first
90 90 end
91 91
92 92 def before_create_cs
93 93 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
94 94 self.comments = self.class.normalize_comments(
95 95 self.comments, repository.repo_log_encoding)
96 96 self.user = repository.find_committer_user(self.committer)
97 97 end
98 98
99 99 def scan_for_issues
100 100 scan_comment_for_issue_ids
101 101 end
102 102
103 103 TIMELOG_RE = /
104 104 (
105 105 ((\d+)(h|hours?))((\d+)(m|min)?)?
106 106 |
107 107 ((\d+)(h|hours?|m|min))
108 108 |
109 109 (\d+):(\d+)
110 110 |
111 111 (\d+([\.,]\d+)?)h?
112 112 )
113 113 /x
114 114
115 115 def scan_comment_for_issue_ids
116 116 return if comments.blank?
117 117 # keywords used to reference issues
118 118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 119 ref_keywords_any = ref_keywords.delete('*')
120 120 # keywords used to fix issues
121 121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
122 122
123 123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124 124
125 125 referenced_issues = []
126 126
127 127 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
128 128 action, refs = match[2].to_s.downcase, match[3]
129 129 next unless action.present? || ref_keywords_any
130 130
131 131 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
132 132 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
133 133 if issue
134 134 referenced_issues << issue
135 135 # Don't update issues or log time when importing old commits
136 136 unless repository.created_on && committed_on && committed_on < repository.created_on
137 137 fix_issue(issue, action) if fix_keywords.include?(action)
138 138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
139 139 end
140 140 end
141 141 end
142 142 end
143 143
144 144 referenced_issues.uniq!
145 145 self.issues = referenced_issues unless referenced_issues.empty?
146 146 end
147 147
148 148 def short_comments
149 149 @short_comments || split_comments.first
150 150 end
151 151
152 152 def long_comments
153 153 @long_comments || split_comments.last
154 154 end
155 155
156 156 def text_tag(ref_project=nil)
157 157 repo = ""
158 158 if repository && repository.identifier.present?
159 159 repo = "#{repository.identifier}|"
160 160 end
161 161 tag = if scmid?
162 162 "commit:#{repo}#{scmid}"
163 163 else
164 164 "#{repo}r#{revision}"
165 165 end
166 166 if ref_project && project && ref_project != project
167 167 tag = "#{project.identifier}:#{tag}"
168 168 end
169 169 tag
170 170 end
171 171
172 172 # Returns the title used for the changeset in the activity/search results
173 173 def title
174 174 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
175 175 comm = short_comments.blank? ? '' : (': ' + short_comments)
176 176 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
177 177 end
178 178
179 179 # Returns the previous changeset
180 180 def previous
181 181 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
182 182 end
183 183
184 184 # Returns the next changeset
185 185 def next
186 186 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
187 187 end
188 188
189 189 # Creates a new Change from it's common parameters
190 190 def create_change(change)
191 191 Change.create(:changeset => self,
192 192 :action => change[:action],
193 193 :path => change[:path],
194 194 :from_path => change[:from_path],
195 195 :from_revision => change[:from_revision])
196 196 end
197 197
198 198 # Finds an issue that can be referenced by the commit message
199 199 def find_referenced_issue_by_id(id)
200 200 return nil if id.blank?
201 201 issue = Issue.includes(:project).where(:id => id.to_i).first
202 202 if Setting.commit_cross_project_ref?
203 203 # all issues can be referenced/fixed
204 204 elsif issue
205 205 # issue that belong to the repository project, a subproject or a parent project only
206 206 unless issue.project &&
207 207 (project == issue.project || project.is_ancestor_of?(issue.project) ||
208 208 project.is_descendant_of?(issue.project))
209 209 issue = nil
210 210 end
211 211 end
212 212 issue
213 213 end
214 214
215 215 private
216 216
217 217 # Updates the +issue+ according to +action+
218 218 def fix_issue(issue, action)
219 219 # the issue may have been updated by the closure of another one (eg. duplicate)
220 220 issue.reload
221 221 # don't change the status is the issue is closed
222 222 return if issue.status && issue.status.is_closed?
223 223
224 224 journal = issue.init_journal(user || User.anonymous,
225 225 ll(Setting.default_language,
226 226 :text_status_changed_by_changeset,
227 227 text_tag(issue.project)))
228 228 rule = Setting.commit_update_keywords_array.detect do |rule|
229 229 rule['keywords'].include?(action) &&
230 230 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
231 231 end
232 232 if rule
233 233 issue.assign_attributes rule.slice(*Issue.attribute_names)
234 234 end
235 235 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
236 236 { :changeset => self, :issue => issue, :action => action })
237 237 unless issue.save
238 238 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
239 239 end
240 240 issue
241 241 end
242 242
243 243 def log_time(issue, hours)
244 244 time_entry = TimeEntry.new(
245 245 :user => user,
246 246 :hours => hours,
247 247 :issue => issue,
248 248 :spent_on => commit_date,
249 249 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
250 250 :locale => Setting.default_language)
251 251 )
252 252 time_entry.activity = log_time_activity unless log_time_activity.nil?
253 253
254 254 unless time_entry.save
255 255 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
256 256 end
257 257 time_entry
258 258 end
259 259
260 260 def log_time_activity
261 261 if Setting.commit_logtime_activity_id.to_i > 0
262 262 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
263 263 end
264 264 end
265 265
266 266 def split_comments
267 267 comments =~ /\A(.+?)\r?\n(.*)$/m
268 268 @short_comments = $1 || comments
269 269 @long_comments = $2.to_s.strip
270 270 return @short_comments, @long_comments
271 271 end
272 272
273 273 public
274 274
275 275 # Strips and reencodes a commit log before insertion into the database
276 276 def self.normalize_comments(str, encoding)
277 277 Changeset.to_utf8(str.to_s.strip, encoding)
278 278 end
279 279
280 280 def self.to_utf8(str, encoding)
281 281 Redmine::CodesetUtil.to_utf8(str, encoding)
282 282 end
283 283 end
@@ -1,37 +1,37
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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', :foreign_key => 'author_id'
22 22
23 23 validates_presence_of :commented, :author, :comments
24 24
25 25 after_create :send_notification
26 26
27 27 safe_attributes 'comments'
28 28
29 29 private
30 30
31 31 def send_notification
32 32 mailer_method = "#{commented.class.name.underscore}_comment_added"
33 33 if Setting.notified_events.include?(mailer_method)
34 34 Mailer.send(mailer_method, self).deliver
35 35 end
36 36 end
37 37 end
@@ -1,283 +1,283
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :custom_values, :dependent => :delete_all
22 22 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
23 23 acts_as_list :scope => 'type = \'#{self.class}\''
24 24 serialize :possible_values
25 25 store :format_store
26 26
27 27 validates_presence_of :name, :field_format
28 28 validates_uniqueness_of :name, :scope => :type
29 29 validates_length_of :name, :maximum => 30
30 30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
31 31 validate :validate_custom_field
32 32
33 33 before_validation :set_searchable
34 34 before_save do |field|
35 35 field.format.before_custom_field_save(field)
36 36 end
37 37 after_save :handle_multiplicity_change
38 38 after_save do |field|
39 39 if field.visible_changed? && field.visible
40 40 field.roles.clear
41 41 end
42 42 end
43 43
44 44 scope :sorted, lambda { order("#{table_name}.position ASC") }
45 45 scope :visible, lambda {|*args|
46 46 user = args.shift || User.current
47 47 if user.admin?
48 48 # nop
49 49 elsif user.memberships.any?
50 50 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
51 51 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
52 52 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
53 53 " WHERE m.user_id = ?)",
54 54 true, user.id)
55 55 else
56 56 where(:visible => true)
57 57 end
58 58 }
59 59
60 60 def visible_by?(project, user=User.current)
61 61 visible? || user.admin?
62 62 end
63 63
64 64 def format
65 65 @format ||= Redmine::FieldFormat.find(field_format)
66 66 end
67 67
68 68 def field_format=(arg)
69 69 # cannot change format of a saved custom field
70 70 if new_record?
71 71 @format = nil
72 72 super
73 73 end
74 74 end
75 75
76 76 def set_searchable
77 77 # make sure these fields are not searchable
78 78 self.searchable = false unless format.class.searchable_supported
79 79 # make sure only these fields can have multiple values
80 80 self.multiple = false unless format.class.multiple_supported
81 81 true
82 82 end
83 83
84 84 def validate_custom_field
85 85 format.validate_custom_field(self).each do |attribute, message|
86 86 errors.add attribute, message
87 87 end
88 88
89 89 if regexp.present?
90 90 begin
91 91 Regexp.new(regexp)
92 92 rescue
93 93 errors.add(:regexp, :invalid)
94 94 end
95 95 end
96 96
97 97 if default_value.present?
98 98 validate_field_value(default_value).each do |message|
99 99 errors.add :default_value, message
100 100 end
101 101 end
102 102 end
103 103
104 104 def possible_custom_value_options(custom_value)
105 105 format.possible_custom_value_options(custom_value)
106 106 end
107 107
108 108 def possible_values_options(object=nil)
109 109 if object.is_a?(Array)
110 110 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
111 111 else
112 112 format.possible_values_options(self, object) || []
113 113 end
114 114 end
115 115
116 116 def possible_values
117 117 values = super()
118 118 if values.is_a?(Array)
119 119 values.each do |value|
120 120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 121 end
122 122 values
123 123 else
124 124 []
125 125 end
126 126 end
127 127
128 128 # Makes possible_values accept a multiline string
129 129 def possible_values=(arg)
130 130 if arg.is_a?(Array)
131 131 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
132 132 else
133 133 self.possible_values = arg.to_s.split(/[\n\r]+/)
134 134 end
135 135 end
136 136
137 137 def cast_value(value)
138 138 format.cast_value(self, value)
139 139 end
140 140
141 141 def value_from_keyword(keyword, customized)
142 142 possible_values_options = possible_values_options(customized)
143 143 if possible_values_options.present?
144 144 keyword = keyword.to_s.downcase
145 145 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
146 146 if v.is_a?(Array)
147 147 v.last
148 148 else
149 149 v
150 150 end
151 151 end
152 152 else
153 153 keyword
154 154 end
155 155 end
156 156
157 157 # Returns a ORDER BY clause that can used to sort customized
158 158 # objects by their value of the custom field.
159 159 # Returns nil if the custom field can not be used for sorting.
160 160 def order_statement
161 161 return nil if multiple?
162 162 format.order_statement(self)
163 163 end
164 164
165 165 # Returns a GROUP BY clause that can used to group by custom value
166 166 # Returns nil if the custom field can not be used for grouping.
167 167 def group_statement
168 168 return nil if multiple?
169 169 format.group_statement(self)
170 170 end
171 171
172 172 def join_for_order_statement
173 173 format.join_for_order_statement(self)
174 174 end
175 175
176 176 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
177 177 if visible? || user.admin?
178 178 "1=1"
179 179 elsif user.anonymous?
180 180 "1=0"
181 181 else
182 182 project_key ||= "#{self.class.customized_class.table_name}.project_id"
183 183 id_column ||= id
184 184 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
185 185 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
186 186 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
187 187 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
188 188 end
189 189 end
190 190
191 191 def self.visibility_condition
192 192 if user.admin?
193 193 "1=1"
194 194 elsif user.anonymous?
195 195 "#{table_name}.visible"
196 196 else
197 197 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
198 198 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
199 199 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
200 200 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
201 201 end
202 202 end
203 203
204 204 def <=>(field)
205 205 position <=> field.position
206 206 end
207 207
208 208 # Returns the class that values represent
209 209 def value_class
210 210 format.target_class if format.respond_to?(:target_class)
211 211 end
212 212
213 213 def self.customized_class
214 214 self.name =~ /^(.+)CustomField$/
215 215 $1.constantize rescue nil
216 216 end
217 217
218 218 # to move in project_custom_field
219 219 def self.for_all
220 220 where(:is_for_all => true).order('position').all
221 221 end
222 222
223 223 def type_name
224 224 nil
225 225 end
226 226
227 227 # Returns the error messages for the given value
228 228 # or an empty array if value is a valid value for the custom field
229 229 def validate_custom_value(custom_value)
230 230 value = custom_value.value
231 231 errs = []
232 232 if value.is_a?(Array)
233 233 if !multiple?
234 234 errs << ::I18n.t('activerecord.errors.messages.invalid')
235 235 end
236 236 if is_required? && value.detect(&:present?).nil?
237 237 errs << ::I18n.t('activerecord.errors.messages.blank')
238 238 end
239 239 else
240 240 if is_required? && value.blank?
241 241 errs << ::I18n.t('activerecord.errors.messages.blank')
242 242 end
243 243 end
244 244 if custom_value.value.present?
245 245 errs += format.validate_custom_value(custom_value)
246 246 end
247 247 errs
248 248 end
249 249
250 250 # Returns the error messages for the default custom field value
251 251 def validate_field_value(value)
252 252 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
253 253 end
254 254
255 255 # Returns true if value is a valid value for the custom field
256 256 def valid_field_value?(value)
257 257 validate_field_value(value).empty?
258 258 end
259 259
260 260 def format_in?(*args)
261 261 args.include?(field_format)
262 262 end
263 263
264 264 protected
265 265
266 266 # Removes multiple values for the custom field after setting the multiple attribute to false
267 267 # We kepp the value with the highest id for each customized object
268 268 def handle_multiplicity_change
269 269 if !new_record? && multiple_was && !multiple
270 270 ids = custom_values.
271 271 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
272 272 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
273 273 " AND cve.id > #{CustomValue.table_name}.id)").
274 274 pluck(:id)
275 275
276 276 if ids.any?
277 277 custom_values.where(:id => ids).delete_all
278 278 end
279 279 end
280 280 end
281 281 end
282 282
283 283 require_dependency 'redmine/field_format'
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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,49 +1,49
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
22 22 def initialize(attributes=nil, *args)
23 23 super
24 24 if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
25 25 self.value ||= custom_field.default_value
26 26 end
27 27 end
28 28
29 29 # Returns true if the boolean custom value is true
30 30 def true?
31 31 self.value == '1'
32 32 end
33 33
34 34 def editable?
35 35 custom_field.editable?
36 36 end
37 37
38 38 def visible?
39 39 custom_field.visible?
40 40 end
41 41
42 42 def required?
43 43 custom_field.is_required?
44 44 end
45 45
46 46 def to_s
47 47 value.to_s
48 48 end
49 49 end
@@ -1,67 +1,67
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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", :foreign_key => "category_id"
22 22 acts_as_attachable :delete_permission => :delete_documents
23 23
24 24 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
25 25 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
26 26 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
27 27 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
28 28 acts_as_activity_provider :find_options => {:include => :project}
29 29
30 30 validates_presence_of :project, :title, :category
31 31 validates_length_of :title, :maximum => 60
32 32
33 33 after_create :send_notification
34 34
35 35 scope :visible, lambda {|*args|
36 36 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
37 37 }
38 38
39 39 safe_attributes 'category_id', 'title', 'description'
40 40
41 41 def visible?(user=User.current)
42 42 !user.nil? && user.allowed_to?(:view_documents, project)
43 43 end
44 44
45 45 def initialize(attributes=nil, *args)
46 46 super
47 47 if new_record?
48 48 self.category ||= DocumentCategory.default
49 49 end
50 50 end
51 51
52 52 def updated_on
53 53 unless @updated_on
54 54 a = attachments.last
55 55 @updated_on = (a && a.created_on) || created_on
56 56 end
57 57 @updated_on
58 58 end
59 59
60 60 private
61 61
62 62 def send_notification
63 63 if Setting.notified_events.include?('document_added')
64 64 Mailer.document_added(self).deliver
65 65 end
66 66 end
67 67 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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,38 +1,38
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
21 21 validates_presence_of :name
22 22 validates_uniqueness_of :name, :scope => :project_id
23 23
24 24 after_create :module_enabled
25 25
26 26 private
27 27
28 28 # after_create callback used to do things when a module is enabled
29 29 def module_enabled
30 30 case name
31 31 when 'wiki'
32 32 # Create a wiki with a default start page
33 33 if project && project.wiki.nil?
34 34 Wiki.create(:project => project, :start_page => 'Wiki')
35 35 end
36 36 end
37 37 end
38 38 end
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 :order => "#{Enumeration.table_name}.position ASC"
22 22
23 23 belongs_to :project
24 24
25 25 acts_as_list :scope => 'type = \'#{type}\''
26 26 acts_as_customizable
27 27 acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
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("#{table_name}.position ASC") }
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 overiding 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.overridding_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 private
133 133 def check_integrity
134 134 raise "Can't delete enumeration" if self.in_use?
135 135 end
136 136
137 137 end
138 138
139 139 # Force load the subclasses in development mode
140 140 require_dependency 'time_entry_activity'
141 141 require_dependency 'document_category'
142 142 require_dependency 'issue_priority'
@@ -1,91 +1,91
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 Group < Principal
19 19 include Redmine::SafeAttributes
20 20
21 21 has_and_belongs_to_many :users,
22 22 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
23 23 :after_add => :user_added,
24 24 :after_remove => :user_removed
25 25
26 26 acts_as_customizable
27 27
28 28 validates_presence_of :lastname
29 29 validates_uniqueness_of :lastname, :case_sensitive => false
30 30 validates_length_of :lastname, :maximum => 255
31 31
32 32 before_destroy :remove_references_before_destroy
33 33
34 34 scope :sorted, lambda { order("#{table_name}.lastname ASC") }
35 35 scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
36 36
37 37 safe_attributes 'name',
38 38 'user_ids',
39 39 'custom_field_values',
40 40 'custom_fields',
41 41 :if => lambda {|group, user| user.admin?}
42 42
43 43 def to_s
44 44 lastname.to_s
45 45 end
46 46
47 47 def name
48 48 lastname
49 49 end
50 50
51 51 def name=(arg)
52 52 self.lastname = arg
53 53 end
54 54
55 55 def user_added(user)
56 56 members.each do |member|
57 57 next if member.project.nil?
58 58 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
59 59 member.member_roles.each do |member_role|
60 60 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
61 61 end
62 62 user_member.save!
63 63 end
64 64 end
65 65
66 66 def user_removed(user)
67 67 members.each do |member|
68 68 MemberRole.
69 69 includes(:member).
70 70 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
71 71 each(&:destroy)
72 72 end
73 73 end
74 74
75 75 def self.human_attribute_name(attribute_key_name, *args)
76 76 attr_name = attribute_key_name.to_s
77 77 if attr_name == 'lastname'
78 78 attr_name = "name"
79 79 end
80 80 super(attr_name, *args)
81 81 end
82 82
83 83 private
84 84
85 85 # Removes references that are not handled by associations
86 86 def remove_references_before_destroy
87 87 return if self.id.nil?
88 88
89 89 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
90 90 end
91 91 end
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 GroupCustomField < CustomField
19 19 def type_name
20 20 :label_group_plural
21 21 end
22 22 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was 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