##// END OF EJS Templates
Copyright update....
Jean-Philippe Lang -
r13490:000124f44f53
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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,77 +1,77
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ActivitiesController < ApplicationController
19 19 menu_item :activity
20 20 before_filter :find_optional_project
21 21 accept_rss_auth :index
22 22
23 23 def index
24 24 @days = Setting.activity_days_default.to_i
25 25
26 26 if params[:from]
27 27 begin; @date_to = params[:from].to_date + 1; rescue; end
28 28 end
29 29
30 30 @date_to ||= Date.today + 1
31 31 @date_from = @date_to - @days
32 32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
33 33 if params[:user_id].present?
34 34 @author = User.active.find(params[:user_id])
35 35 end
36 36
37 37 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
38 38 :with_subprojects => @with_subprojects,
39 39 :author => @author)
40 40 @activity.scope_select {|t| !params["show_#{t}"].nil?}
41 41 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
42 42
43 43 events = @activity.events(@date_from, @date_to)
44 44
45 45 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
46 46 respond_to do |format|
47 47 format.html {
48 48 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
49 49 render :layout => false if request.xhr?
50 50 }
51 51 format.atom {
52 52 title = l(:label_activity)
53 53 if @author
54 54 title = @author.name
55 55 elsif @activity.scope.size == 1
56 56 title = l("label_#{@activity.scope.first.singularize}_plural")
57 57 end
58 58 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
59 59 }
60 60 end
61 61 end
62 62
63 63 rescue ActiveRecord::RecordNotFound
64 64 render_404
65 65 end
66 66
67 67 private
68 68
69 69 # TODO: refactor, duplicated in projects_controller
70 70 def find_optional_project
71 71 return true unless params[:id]
72 72 @project = Project.find(params[:id])
73 73 authorize
74 74 rescue ActiveRecord::RecordNotFound
75 75 render_404
76 76 end
77 77 end
@@ -1,84 +1,84
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AdminController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :projects, :only => :projects
21 21 menu_item :plugins, :only => :plugins
22 22 menu_item :info, :only => :info
23 23
24 24 before_filter :require_admin
25 25 helper :sort
26 26 include SortHelper
27 27
28 28 def index
29 29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 30 end
31 31
32 32 def projects
33 33 @status = params[:status] || 1
34 34
35 35 scope = Project.status(@status).sorted
36 36 scope = scope.like(params[:name]) if params[:name].present?
37 37 @projects = scope.to_a
38 38
39 39 render :action => "projects", :layout => false if request.xhr?
40 40 end
41 41
42 42 def plugins
43 43 @plugins = Redmine::Plugin.all
44 44 end
45 45
46 46 # Loads the default configuration
47 47 # (roles, trackers, statuses, workflow, enumerations)
48 48 def default_configuration
49 49 if request.post?
50 50 begin
51 51 Redmine::DefaultData::Loader::load(params[:lang])
52 52 flash[:notice] = l(:notice_default_data_loaded)
53 53 rescue Exception => e
54 54 flash[:error] = l(:error_can_t_load_default_data, 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.dup))
69 69 end
70 70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
71 71 redirect_to settings_path(:tab => 'notifications')
72 72 end
73 73
74 74 def info
75 75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
76 76 @checklist = [
77 77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
78 78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
79 79 ["#{l :text_plugin_assets_writable} (./public/plugin_assets)", File.writable?(Redmine::Plugin.public_directory)],
80 80 [:text_rmagick_available, Object.const_defined?(:Magick)],
81 81 [:text_convert_available, Redmine::Thumbnail.convert_available?]
82 82 ]
83 83 end
84 84 end
@@ -1,656 +1,656
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 self.logged_user = nil
48 48 set_localization
49 49 render_error :status => 422, :message => "Invalid form authenticity token."
50 50 end
51 51 end
52 52
53 53 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54 54
55 55 rescue_from ::Unauthorized, :with => :deny_access
56 56 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57 57
58 58 include Redmine::Search::Controller
59 59 include Redmine::MenuManager::MenuController
60 60 helper Redmine::MenuManager::MenuHelper
61 61
62 62 def session_expiration
63 63 if session[:user_id]
64 64 if session_expired? && !try_to_autologin
65 65 set_localization(User.active.find_by_id(session[:user_id]))
66 66 reset_session
67 67 flash[:error] = l(:error_session_expired)
68 68 redirect_to signin_url
69 69 else
70 70 session[:atime] = Time.now.utc.to_i
71 71 end
72 72 end
73 73 end
74 74
75 75 def session_expired?
76 76 if Setting.session_lifetime?
77 77 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
78 78 return true
79 79 end
80 80 end
81 81 if Setting.session_timeout?
82 82 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
83 83 return true
84 84 end
85 85 end
86 86 false
87 87 end
88 88
89 89 def start_user_session(user)
90 90 session[:user_id] = user.id
91 91 session[:ctime] = Time.now.utc.to_i
92 92 session[:atime] = Time.now.utc.to_i
93 93 if user.must_change_password?
94 94 session[:pwd] = '1'
95 95 end
96 96 end
97 97
98 98 def user_setup
99 99 # Check the settings cache for each request
100 100 Setting.check_cache
101 101 # Find the current user
102 102 User.current = find_current_user
103 103 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
104 104 end
105 105
106 106 # Returns the current user or nil if no user is logged in
107 107 # and starts a session if needed
108 108 def find_current_user
109 109 user = nil
110 110 unless api_request?
111 111 if session[:user_id]
112 112 # existing session
113 113 user = (User.active.find(session[:user_id]) rescue nil)
114 114 elsif autologin_user = try_to_autologin
115 115 user = autologin_user
116 116 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
117 117 # RSS key authentication does not start a session
118 118 user = User.find_by_rss_key(params[:key])
119 119 end
120 120 end
121 121 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
122 122 if (key = api_key_from_request)
123 123 # Use API key
124 124 user = User.find_by_api_key(key)
125 125 elsif request.authorization.to_s =~ /\ABasic /i
126 126 # HTTP Basic, either username/password or API key/random
127 127 authenticate_with_http_basic do |username, password|
128 128 user = User.try_to_login(username, password) || User.find_by_api_key(username)
129 129 end
130 130 if user && user.must_change_password?
131 131 render_error :message => 'You must change your password', :status => 403
132 132 return
133 133 end
134 134 end
135 135 # Switch user if requested by an admin user
136 136 if user && user.admin? && (username = api_switch_user_from_request)
137 137 su = User.find_by_login(username)
138 138 if su && su.active?
139 139 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
140 140 user = su
141 141 else
142 142 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
143 143 end
144 144 end
145 145 end
146 146 user
147 147 end
148 148
149 149 def force_logout_if_password_changed
150 150 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
151 151 # Make sure we force logout only for web browser sessions, not API calls
152 152 # if the password was changed after the session creation.
153 153 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
154 154 reset_session
155 155 set_localization
156 156 flash[:error] = l(:error_session_expired)
157 157 redirect_to signin_url
158 158 end
159 159 end
160 160
161 161 def autologin_cookie_name
162 162 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
163 163 end
164 164
165 165 def try_to_autologin
166 166 if cookies[autologin_cookie_name] && Setting.autologin?
167 167 # auto-login feature starts a new session
168 168 user = User.try_to_autologin(cookies[autologin_cookie_name])
169 169 if user
170 170 reset_session
171 171 start_user_session(user)
172 172 end
173 173 user
174 174 end
175 175 end
176 176
177 177 # Sets the logged in user
178 178 def logged_user=(user)
179 179 reset_session
180 180 if user && user.is_a?(User)
181 181 User.current = user
182 182 start_user_session(user)
183 183 else
184 184 User.current = User.anonymous
185 185 end
186 186 end
187 187
188 188 # Logs out current user
189 189 def logout_user
190 190 if User.current.logged?
191 191 cookies.delete(autologin_cookie_name)
192 192 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
193 193 self.logged_user = nil
194 194 end
195 195 end
196 196
197 197 # check if login is globally required to access the application
198 198 def check_if_login_required
199 199 # no check needed if user is already logged in
200 200 return true if User.current.logged?
201 201 require_login if Setting.login_required?
202 202 end
203 203
204 204 def check_password_change
205 205 if session[:pwd]
206 206 if User.current.must_change_password?
207 207 redirect_to my_password_path
208 208 else
209 209 session.delete(:pwd)
210 210 end
211 211 end
212 212 end
213 213
214 214 def set_localization(user=User.current)
215 215 lang = nil
216 216 if user && user.logged?
217 217 lang = find_language(user.language)
218 218 end
219 219 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
220 220 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
221 221 if !accept_lang.blank?
222 222 accept_lang = accept_lang.downcase
223 223 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
224 224 end
225 225 end
226 226 lang ||= Setting.default_language
227 227 set_language_if_valid(lang)
228 228 end
229 229
230 230 def require_login
231 231 if !User.current.logged?
232 232 # Extract only the basic url parameters on non-GET requests
233 233 if request.get?
234 234 url = url_for(params)
235 235 else
236 236 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
237 237 end
238 238 respond_to do |format|
239 239 format.html {
240 240 if request.xhr?
241 241 head :unauthorized
242 242 else
243 243 redirect_to :controller => "account", :action => "login", :back_url => url
244 244 end
245 245 }
246 246 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
247 247 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
248 248 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
249 249 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
250 250 end
251 251 return false
252 252 end
253 253 true
254 254 end
255 255
256 256 def require_admin
257 257 return unless require_login
258 258 if !User.current.admin?
259 259 render_403
260 260 return false
261 261 end
262 262 true
263 263 end
264 264
265 265 def deny_access
266 266 User.current.logged? ? render_403 : require_login
267 267 end
268 268
269 269 # Authorize the user for the requested action
270 270 def authorize(ctrl = params[:controller], action = params[:action], global = false)
271 271 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
272 272 if allowed
273 273 true
274 274 else
275 275 if @project && @project.archived?
276 276 render_403 :message => :notice_not_authorized_archived_project
277 277 else
278 278 deny_access
279 279 end
280 280 end
281 281 end
282 282
283 283 # Authorize the user for the requested action outside a project
284 284 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
285 285 authorize(ctrl, action, global)
286 286 end
287 287
288 288 # Find project of id params[:id]
289 289 def find_project
290 290 @project = Project.find(params[:id])
291 291 rescue ActiveRecord::RecordNotFound
292 292 render_404
293 293 end
294 294
295 295 # Find project of id params[:project_id]
296 296 def find_project_by_project_id
297 297 @project = Project.find(params[:project_id])
298 298 rescue ActiveRecord::RecordNotFound
299 299 render_404
300 300 end
301 301
302 302 # Find a project based on params[:project_id]
303 303 # TODO: some subclasses override this, see about merging their logic
304 304 def find_optional_project
305 305 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
306 306 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
307 307 allowed ? true : deny_access
308 308 rescue ActiveRecord::RecordNotFound
309 309 render_404
310 310 end
311 311
312 312 # Finds and sets @project based on @object.project
313 313 def find_project_from_association
314 314 render_404 unless @object.present?
315 315
316 316 @project = @object.project
317 317 end
318 318
319 319 def find_model_object
320 320 model = self.class.model_object
321 321 if model
322 322 @object = model.find(params[:id])
323 323 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
324 324 end
325 325 rescue ActiveRecord::RecordNotFound
326 326 render_404
327 327 end
328 328
329 329 def self.model_object(model)
330 330 self.model_object = model
331 331 end
332 332
333 333 # Find the issue whose id is the :id parameter
334 334 # Raises a Unauthorized exception if the issue is not visible
335 335 def find_issue
336 336 # Issue.visible.find(...) can not be used to redirect user to the login form
337 337 # if the issue actually exists but requires authentication
338 338 @issue = Issue.find(params[:id])
339 339 raise Unauthorized unless @issue.visible?
340 340 @project = @issue.project
341 341 rescue ActiveRecord::RecordNotFound
342 342 render_404
343 343 end
344 344
345 345 # Find issues with a single :id param or :ids array param
346 346 # Raises a Unauthorized exception if one of the issues is not visible
347 347 def find_issues
348 348 @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
349 349 raise ActiveRecord::RecordNotFound if @issues.empty?
350 350 raise Unauthorized unless @issues.all?(&:visible?)
351 351 @projects = @issues.collect(&:project).compact.uniq
352 352 @project = @projects.first if @projects.size == 1
353 353 rescue ActiveRecord::RecordNotFound
354 354 render_404
355 355 end
356 356
357 357 def find_attachments
358 358 if (attachments = params[:attachments]).present?
359 359 att = attachments.values.collect do |attachment|
360 360 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
361 361 end
362 362 att.compact!
363 363 end
364 364 @attachments = att || []
365 365 end
366 366
367 367 # make sure that the user is a member of the project (or admin) if project is private
368 368 # used as a before_filter for actions that do not require any particular permission on the project
369 369 def check_project_privacy
370 370 if @project && !@project.archived?
371 371 if @project.visible?
372 372 true
373 373 else
374 374 deny_access
375 375 end
376 376 else
377 377 @project = nil
378 378 render_404
379 379 false
380 380 end
381 381 end
382 382
383 383 def back_url
384 384 url = params[:back_url]
385 385 if url.nil? && referer = request.env['HTTP_REFERER']
386 386 url = CGI.unescape(referer.to_s)
387 387 end
388 388 url
389 389 end
390 390
391 391 def redirect_back_or_default(default, options={})
392 392 back_url = params[:back_url].to_s
393 393 if back_url.present? && valid_back_url?(back_url)
394 394 redirect_to(back_url)
395 395 return
396 396 elsif options[:referer]
397 397 redirect_to_referer_or default
398 398 return
399 399 end
400 400 redirect_to default
401 401 false
402 402 end
403 403
404 404 # Returns true if back_url is a valid url for redirection, otherwise false
405 405 def valid_back_url?(back_url)
406 406 if CGI.unescape(back_url).include?('..')
407 407 return false
408 408 end
409 409
410 410 begin
411 411 uri = URI.parse(back_url)
412 412 rescue URI::InvalidURIError
413 413 return false
414 414 end
415 415
416 416 if uri.host.present? && uri.host != request.host
417 417 return false
418 418 end
419 419
420 420 if uri.path.match(%r{/(login|account/register)})
421 421 return false
422 422 end
423 423
424 424 if relative_url_root.present? && !uri.path.starts_with?(relative_url_root)
425 425 return false
426 426 end
427 427
428 428 return true
429 429 end
430 430 private :valid_back_url?
431 431
432 432 # Redirects to the request referer if present, redirects to args or call block otherwise.
433 433 def redirect_to_referer_or(*args, &block)
434 434 redirect_to :back
435 435 rescue ::ActionController::RedirectBackError
436 436 if args.any?
437 437 redirect_to *args
438 438 elsif block_given?
439 439 block.call
440 440 else
441 441 raise "#redirect_to_referer_or takes arguments or a block"
442 442 end
443 443 end
444 444
445 445 def render_403(options={})
446 446 @project = nil
447 447 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
448 448 return false
449 449 end
450 450
451 451 def render_404(options={})
452 452 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
453 453 return false
454 454 end
455 455
456 456 # Renders an error response
457 457 def render_error(arg)
458 458 arg = {:message => arg} unless arg.is_a?(Hash)
459 459
460 460 @message = arg[:message]
461 461 @message = l(@message) if @message.is_a?(Symbol)
462 462 @status = arg[:status] || 500
463 463
464 464 respond_to do |format|
465 465 format.html {
466 466 render :template => 'common/error', :layout => use_layout, :status => @status
467 467 }
468 468 format.any { head @status }
469 469 end
470 470 end
471 471
472 472 # Handler for ActionView::MissingTemplate exception
473 473 def missing_template
474 474 logger.warn "Missing template, responding with 404"
475 475 @project = nil
476 476 render_404
477 477 end
478 478
479 479 # Filter for actions that provide an API response
480 480 # but have no HTML representation for non admin users
481 481 def require_admin_or_api_request
482 482 return true if api_request?
483 483 if User.current.admin?
484 484 true
485 485 elsif User.current.logged?
486 486 render_error(:status => 406)
487 487 else
488 488 deny_access
489 489 end
490 490 end
491 491
492 492 # Picks which layout to use based on the request
493 493 #
494 494 # @return [boolean, string] name of the layout to use or false for no layout
495 495 def use_layout
496 496 request.xhr? ? false : 'base'
497 497 end
498 498
499 499 def render_feed(items, options={})
500 500 @items = (items || []).to_a
501 501 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
502 502 @items = @items.slice(0, Setting.feeds_limit.to_i)
503 503 @title = options[:title] || Setting.app_title
504 504 render :template => "common/feed", :formats => [:atom], :layout => false,
505 505 :content_type => 'application/atom+xml'
506 506 end
507 507
508 508 def self.accept_rss_auth(*actions)
509 509 if actions.any?
510 510 self.accept_rss_auth_actions = actions
511 511 else
512 512 self.accept_rss_auth_actions || []
513 513 end
514 514 end
515 515
516 516 def accept_rss_auth?(action=action_name)
517 517 self.class.accept_rss_auth.include?(action.to_sym)
518 518 end
519 519
520 520 def self.accept_api_auth(*actions)
521 521 if actions.any?
522 522 self.accept_api_auth_actions = actions
523 523 else
524 524 self.accept_api_auth_actions || []
525 525 end
526 526 end
527 527
528 528 def accept_api_auth?(action=action_name)
529 529 self.class.accept_api_auth.include?(action.to_sym)
530 530 end
531 531
532 532 # Returns the number of objects that should be displayed
533 533 # on the paginated list
534 534 def per_page_option
535 535 per_page = nil
536 536 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
537 537 per_page = params[:per_page].to_s.to_i
538 538 session[:per_page] = per_page
539 539 elsif session[:per_page]
540 540 per_page = session[:per_page]
541 541 else
542 542 per_page = Setting.per_page_options_array.first || 25
543 543 end
544 544 per_page
545 545 end
546 546
547 547 # Returns offset and limit used to retrieve objects
548 548 # for an API response based on offset, limit and page parameters
549 549 def api_offset_and_limit(options=params)
550 550 if options[:offset].present?
551 551 offset = options[:offset].to_i
552 552 if offset < 0
553 553 offset = 0
554 554 end
555 555 end
556 556 limit = options[:limit].to_i
557 557 if limit < 1
558 558 limit = 25
559 559 elsif limit > 100
560 560 limit = 100
561 561 end
562 562 if offset.nil? && options[:page].present?
563 563 offset = (options[:page].to_i - 1) * limit
564 564 offset = 0 if offset < 0
565 565 end
566 566 offset ||= 0
567 567
568 568 [offset, limit]
569 569 end
570 570
571 571 # qvalues http header parser
572 572 # code taken from webrick
573 573 def parse_qvalues(value)
574 574 tmp = []
575 575 if value
576 576 parts = value.split(/,\s*/)
577 577 parts.each {|part|
578 578 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
579 579 val = m[1]
580 580 q = (m[2] or 1).to_f
581 581 tmp.push([val, q])
582 582 end
583 583 }
584 584 tmp = tmp.sort_by{|val, q| -q}
585 585 tmp.collect!{|val, q| val}
586 586 end
587 587 return tmp
588 588 rescue
589 589 nil
590 590 end
591 591
592 592 # Returns a string that can be used as filename value in Content-Disposition header
593 593 def filename_for_content_disposition(name)
594 594 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
595 595 end
596 596
597 597 def api_request?
598 598 %w(xml json).include? params[:format]
599 599 end
600 600
601 601 # Returns the API key present in the request
602 602 def api_key_from_request
603 603 if params[:key].present?
604 604 params[:key].to_s
605 605 elsif request.headers["X-Redmine-API-Key"].present?
606 606 request.headers["X-Redmine-API-Key"].to_s
607 607 end
608 608 end
609 609
610 610 # Returns the API 'switch user' value if present
611 611 def api_switch_user_from_request
612 612 request.headers["X-Redmine-Switch-User"].to_s.presence
613 613 end
614 614
615 615 # Renders a warning flash if obj has unsaved attachments
616 616 def render_attachment_warning_if_needed(obj)
617 617 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
618 618 end
619 619
620 620 # Rescues an invalid query statement. Just in case...
621 621 def query_statement_invalid(exception)
622 622 logger.error "Query::StatementInvalid: #{exception.message}" if logger
623 623 session.delete(:query)
624 624 sort_clear if respond_to?(:sort_clear)
625 625 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
626 626 end
627 627
628 628 # Renders a 200 response for successfull updates or deletions via the API
629 629 def render_api_ok
630 630 render_api_head :ok
631 631 end
632 632
633 633 # Renders a head API response
634 634 def render_api_head(status)
635 635 # #head would return a response body with one space
636 636 render :text => '', :status => status, :layout => nil
637 637 end
638 638
639 639 # Renders API response on validation failure
640 640 # for an object or an array of objects
641 641 def render_validation_errors(objects)
642 642 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
643 643 render_api_errors(messages)
644 644 end
645 645
646 646 def render_api_errors(*messages)
647 647 @error_messages = messages.flatten
648 648 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
649 649 end
650 650
651 651 # Overrides #_include_layout? so that #render with no arguments
652 652 # doesn't use the layout for api requests
653 653 def _include_layout?(*args)
654 654 api_request? ? false : super
655 655 end
656 656 end
@@ -1,191 +1,191
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AttachmentsController < ApplicationController
19 19 before_filter :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
20 20 before_filter :find_editable_attachments, :only => [:edit, :update]
21 21 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
22 22 before_filter :delete_authorize, :only => :destroy
23 23 before_filter :authorize_global, :only => :upload
24 24
25 25 accept_api_auth :show, :download, :thumbnail, :upload
26 26
27 27 def show
28 28 respond_to do |format|
29 29 format.html {
30 30 if @attachment.is_diff?
31 31 @diff = File.new(@attachment.diskfile, "rb").read
32 32 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
33 33 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
34 34 # Save diff type as user preference
35 35 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
36 36 User.current.pref[:diff_type] = @diff_type
37 37 User.current.preference.save
38 38 end
39 39 render :action => 'diff'
40 40 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
41 41 @content = File.new(@attachment.diskfile, "rb").read
42 42 render :action => 'file'
43 43 else
44 44 download
45 45 end
46 46 }
47 47 format.api
48 48 end
49 49 end
50 50
51 51 def download
52 52 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
53 53 @attachment.increment_download
54 54 end
55 55
56 56 if stale?(:etag => @attachment.digest)
57 57 # images are sent inline
58 58 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
59 59 :type => detect_content_type(@attachment),
60 60 :disposition => (@attachment.image? ? 'inline' : 'attachment')
61 61 end
62 62 end
63 63
64 64 def thumbnail
65 65 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
66 66 if stale?(:etag => tbnail)
67 67 send_file tbnail,
68 68 :filename => filename_for_content_disposition(@attachment.filename),
69 69 :type => detect_content_type(@attachment),
70 70 :disposition => 'inline'
71 71 end
72 72 else
73 73 # No thumbnail for the attachment or thumbnail could not be created
74 74 render :nothing => true, :status => 404
75 75 end
76 76 end
77 77
78 78 def upload
79 79 # Make sure that API users get used to set this content type
80 80 # as it won't trigger Rails' automatic parsing of the request body for parameters
81 81 unless request.content_type == 'application/octet-stream'
82 82 render :nothing => true, :status => 406
83 83 return
84 84 end
85 85
86 86 @attachment = Attachment.new(:file => request.raw_post)
87 87 @attachment.author = User.current
88 88 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
89 89 @attachment.content_type = params[:content_type].presence
90 90 saved = @attachment.save
91 91
92 92 respond_to do |format|
93 93 format.js
94 94 format.api {
95 95 if saved
96 96 render :action => 'upload', :status => :created
97 97 else
98 98 render_validation_errors(@attachment)
99 99 end
100 100 }
101 101 end
102 102 end
103 103
104 104 def edit
105 105 end
106 106
107 107 def update
108 108 if params[:attachments].is_a?(Hash)
109 109 if Attachment.update_attachments(@attachments, params[:attachments])
110 110 redirect_back_or_default home_path
111 111 return
112 112 end
113 113 end
114 114 render :action => 'edit'
115 115 end
116 116
117 117 def destroy
118 118 if @attachment.container.respond_to?(:init_journal)
119 119 @attachment.container.init_journal(User.current)
120 120 end
121 121 if @attachment.container
122 122 # Make sure association callbacks are called
123 123 @attachment.container.attachments.delete(@attachment)
124 124 else
125 125 @attachment.destroy
126 126 end
127 127
128 128 respond_to do |format|
129 129 format.html { redirect_to_referer_or project_path(@project) }
130 130 format.js
131 131 end
132 132 end
133 133
134 134 private
135 135
136 136 def find_attachment
137 137 @attachment = Attachment.find(params[:id])
138 138 # Show 404 if the filename in the url is wrong
139 139 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
140 140 @project = @attachment.project
141 141 rescue ActiveRecord::RecordNotFound
142 142 render_404
143 143 end
144 144
145 145 def find_editable_attachments
146 146 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
147 147 unless klass && klass.reflect_on_association(:attachments)
148 148 render_404
149 149 return
150 150 end
151 151
152 152 @container = klass.find(params[:object_id])
153 153 if @container.respond_to?(:visible?) && !@container.visible?
154 154 render_403
155 155 return
156 156 end
157 157 @attachments = @container.attachments.select(&:editable?)
158 158 if @container.respond_to?(:project)
159 159 @project = @container.project
160 160 end
161 161 render_404 if @attachments.empty?
162 162 rescue ActiveRecord::RecordNotFound
163 163 render_404
164 164 end
165 165
166 166 # Checks that the file exists and is readable
167 167 def file_readable
168 168 if @attachment.readable?
169 169 true
170 170 else
171 171 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
172 172 render_404
173 173 end
174 174 end
175 175
176 176 def read_authorize
177 177 @attachment.visible? ? true : deny_access
178 178 end
179 179
180 180 def delete_authorize
181 181 @attachment.deletable? ? true : deny_access
182 182 end
183 183
184 184 def detect_content_type(attachment)
185 185 content_type = attachment.content_type
186 186 if content_type.blank?
187 187 content_type = Redmine::MimeType.of(attachment.filename)
188 188 end
189 189 content_type.to_s
190 190 end
191 191 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AutoCompletesController < ApplicationController
19 19 before_filter :find_project
20 20
21 21 def issues
22 22 @issues = []
23 23 q = (params[:q] || params[:term]).to_s.strip
24 24 if q.present?
25 25 scope = Issue.cross_project_scope(@project, params[:scope]).visible
26 26 if q.match(/\A#?(\d+)\z/)
27 27 @issues << scope.find_by_id($1.to_i)
28 28 end
29 29 @issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE LOWER(?)", "%#{q}%").order("#{Issue.table_name}.id DESC").limit(10).to_a
30 30 @issues.compact!
31 31 end
32 32 render :layout => false
33 33 end
34 34
35 35 private
36 36
37 37 def find_project
38 38 if params[:project_id].present?
39 39 @project = Project.find(params[:project_id])
40 40 end
41 41 rescue ActiveRecord::RecordNotFound
42 42 render_404
43 43 end
44 44 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class BoardsController < ApplicationController
19 19 default_search_scope :messages
20 20 before_filter :find_project_by_project_id, :find_board_if_available, :authorize
21 21 accept_rss_auth :index, :show
22 22
23 23 helper :sort
24 24 include SortHelper
25 25 helper :watchers
26 26
27 27 def index
28 28 @boards = @project.boards.preload(:project, :last_message => :author).to_a
29 29 # show the board if there is only one
30 30 if @boards.size == 1
31 31 @board = @boards.first
32 32 show
33 33 end
34 34 end
35 35
36 36 def show
37 37 respond_to do |format|
38 38 format.html {
39 39 sort_init 'updated_on', 'desc'
40 40 sort_update 'created_on' => "#{Message.table_name}.created_on",
41 41 'replies' => "#{Message.table_name}.replies_count",
42 42 'updated_on' => "COALESCE(last_replies_messages.created_on, #{Message.table_name}.created_on)"
43 43
44 44 @topic_count = @board.topics.count
45 45 @topic_pages = Paginator.new @topic_count, per_page_option, params['page']
46 46 @topics = @board.topics.
47 47 reorder("#{Message.table_name}.sticky DESC").
48 48 joins("LEFT OUTER JOIN #{Message.table_name} last_replies_messages ON last_replies_messages.id = #{Message.table_name}.last_reply_id").
49 49 limit(@topic_pages.per_page).
50 50 offset(@topic_pages.offset).
51 51 order(sort_clause).
52 52 preload(:author, {:last_reply => :author}).
53 53 to_a
54 54 @message = Message.new(:board => @board)
55 55 render :action => 'show', :layout => !request.xhr?
56 56 }
57 57 format.atom {
58 58 @messages = @board.messages.
59 59 reorder('created_on DESC').
60 60 includes(:author, :board).
61 61 limit(Setting.feeds_limit.to_i).
62 62 to_a
63 63 render_feed(@messages, :title => "#{@project}: #{@board}")
64 64 }
65 65 end
66 66 end
67 67
68 68 def new
69 69 @board = @project.boards.build
70 70 @board.safe_attributes = params[:board]
71 71 end
72 72
73 73 def create
74 74 @board = @project.boards.build
75 75 @board.safe_attributes = params[:board]
76 76 if @board.save
77 77 flash[:notice] = l(:notice_successful_create)
78 78 redirect_to_settings_in_projects
79 79 else
80 80 render :action => 'new'
81 81 end
82 82 end
83 83
84 84 def edit
85 85 end
86 86
87 87 def update
88 88 @board.safe_attributes = params[:board]
89 89 if @board.save
90 90 redirect_to_settings_in_projects
91 91 else
92 92 render :action => 'edit'
93 93 end
94 94 end
95 95
96 96 def destroy
97 97 @board.destroy
98 98 redirect_to_settings_in_projects
99 99 end
100 100
101 101 private
102 102 def redirect_to_settings_in_projects
103 103 redirect_to settings_project_path(@project, :tab => 'boards')
104 104 end
105 105
106 106 def find_board_if_available
107 107 @board = @project.boards.find(params[:id]) if params[:id]
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,99 +1,99
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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(&:editable_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 if (@time_entries.size == 1)
75 75 @time_entry = @time_entries.first
76 76 end
77 77
78 78 @projects = @time_entries.collect(&:project).compact.uniq
79 79 @project = @projects.first if @projects.size == 1
80 80 @activities = TimeEntryActivity.shared.active
81 81 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
82 82 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
83 83 }
84 84 @back = back_url
85 85
86 86 @options_by_custom_field = {}
87 87 if @can[:edit]
88 88 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
89 89 custom_fields.each do |field|
90 90 values = field.possible_values_options(@projects)
91 91 if values.present?
92 92 @options_by_custom_field[field] = values
93 93 end
94 94 end
95 95 end
96 96
97 97 render :layout => false
98 98 end
99 99 end
@@ -1,88 +1,88
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomFieldsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :build_new_custom_field, :only => [:new, :create]
23 23 before_filter :find_custom_field, :only => [:edit, :update, :destroy]
24 24 accept_api_auth :index
25 25
26 26 def index
27 27 respond_to do |format|
28 28 format.html {
29 29 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
30 30 }
31 31 format.api {
32 32 @custom_fields = CustomField.all
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
39 39 @custom_field.default_value = nil
40 40 end
41 41
42 42 def create
43 43 if @custom_field.save
44 44 flash[:notice] = l(:notice_successful_create)
45 45 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
46 46 redirect_to custom_fields_path(:tab => @custom_field.class.name)
47 47 else
48 48 render :action => 'new'
49 49 end
50 50 end
51 51
52 52 def edit
53 53 end
54 54
55 55 def update
56 56 if @custom_field.update_attributes(params[:custom_field])
57 57 flash[:notice] = l(:notice_successful_update)
58 58 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
59 59 redirect_to custom_fields_path(:tab => @custom_field.class.name)
60 60 else
61 61 render :action => 'edit'
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 begin
67 67 @custom_field.destroy
68 68 rescue
69 69 flash[:error] = l(:error_can_not_delete_custom_field)
70 70 end
71 71 redirect_to custom_fields_path(:tab => @custom_field.class.name)
72 72 end
73 73
74 74 private
75 75
76 76 def build_new_custom_field
77 77 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
78 78 if @custom_field.nil?
79 79 render :action => 'select_type'
80 80 end
81 81 end
82 82
83 83 def find_custom_field
84 84 @custom_field = CustomField.find(params[:id])
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88 end
@@ -1,94 +1,94
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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).to_a
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.to_a
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 @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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EnumerationsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 before_filter :build_new_enumeration, :only => [:new, :create]
24 24 before_filter :find_enumeration, :only => [:edit, :update, :destroy]
25 25 accept_api_auth :index
26 26
27 27 helper :custom_fields
28 28
29 29 def index
30 30 respond_to do |format|
31 31 format.html
32 32 format.api {
33 33 @klass = Enumeration.get_subclass(params[:type])
34 34 if @klass
35 35 @enumerations = @klass.shared.sorted.to_a
36 36 else
37 37 render_404
38 38 end
39 39 }
40 40 end
41 41 end
42 42
43 43 def new
44 44 end
45 45
46 46 def create
47 47 if request.post? && @enumeration.save
48 48 flash[:notice] = l(:notice_successful_create)
49 49 redirect_to enumerations_path
50 50 else
51 51 render :action => 'new'
52 52 end
53 53 end
54 54
55 55 def edit
56 56 end
57 57
58 58 def update
59 59 if @enumeration.update_attributes(params[:enumeration])
60 60 flash[:notice] = l(:notice_successful_update)
61 61 redirect_to enumerations_path
62 62 else
63 63 render :action => 'edit'
64 64 end
65 65 end
66 66
67 67 def destroy
68 68 if !@enumeration.in_use?
69 69 # No associated objects
70 70 @enumeration.destroy
71 71 redirect_to enumerations_path
72 72 return
73 73 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
74 74 @enumeration.destroy(reassign_to)
75 75 redirect_to enumerations_path
76 76 return
77 77 end
78 78 @enumerations = @enumeration.class.system.to_a - [@enumeration]
79 79 end
80 80
81 81 private
82 82
83 83 def build_new_enumeration
84 84 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
85 85 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
86 86 if @enumeration.nil?
87 87 render_404
88 88 end
89 89 end
90 90
91 91 def find_enumeration
92 92 @enumeration = Enumeration.find(params[:id])
93 93 rescue ActiveRecord::RecordNotFound
94 94 render_404
95 95 end
96 96 end
@@ -1,55 +1,55
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class FilesController < ApplicationController
19 19 menu_item :files
20 20
21 21 before_filter :find_project_by_project_id
22 22 before_filter :authorize
23 23
24 24 helper :sort
25 25 include SortHelper
26 26
27 27 def index
28 28 sort_init 'filename', 'asc'
29 29 sort_update 'filename' => "#{Attachment.table_name}.filename",
30 30 'created_on' => "#{Attachment.table_name}.created_on",
31 31 'size' => "#{Attachment.table_name}.filesize",
32 32 'downloads' => "#{Attachment.table_name}.downloads"
33 33
34 34 @containers = [Project.includes(:attachments).
35 35 references(:attachments).reorder(sort_clause).find(@project.id)]
36 36 @containers += @project.versions.includes(:attachments).
37 37 references(:attachments).reorder(sort_clause).to_a.sort.reverse
38 38 render :layout => !request.xhr?
39 39 end
40 40
41 41 def new
42 42 @versions = @project.versions.sort
43 43 end
44 44
45 45 def create
46 46 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
47 47 attachments = Attachment.attach_files(container, params[:attachments])
48 48 render_attachment_warning_if_needed(container)
49 49
50 50 if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
51 51 Mailer.attachments_added(attachments[:files]).deliver
52 52 end
53 53 redirect_to project_files_path(@project)
54 54 end
55 55 end
@@ -1,48 +1,48
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,147 +1,147
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 helper :principal_memberships
27 27
28 28 def index
29 29 respond_to do |format|
30 30 format.html {
31 31 @groups = Group.sorted.to_a
32 32 @user_count_by_group_id = user_count_by_group_id
33 33 }
34 34 format.api {
35 35 scope = Group.sorted
36 36 scope = scope.givable unless params[:builtin] == '1'
37 37 @groups = scope.to_a
38 38 }
39 39 end
40 40 end
41 41
42 42 def show
43 43 respond_to do |format|
44 44 format.html
45 45 format.api
46 46 end
47 47 end
48 48
49 49 def new
50 50 @group = Group.new
51 51 end
52 52
53 53 def create
54 54 @group = Group.new
55 55 @group.safe_attributes = params[:group]
56 56
57 57 respond_to do |format|
58 58 if @group.save
59 59 format.html {
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to(params[:continue] ? new_group_path : groups_path)
62 62 }
63 63 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
64 64 else
65 65 format.html { render :action => "new" }
66 66 format.api { render_validation_errors(@group) }
67 67 end
68 68 end
69 69 end
70 70
71 71 def edit
72 72 end
73 73
74 74 def update
75 75 @group.safe_attributes = params[:group]
76 76
77 77 respond_to do |format|
78 78 if @group.save
79 79 flash[:notice] = l(:notice_successful_update)
80 80 format.html { redirect_to(groups_path) }
81 81 format.api { render_api_ok }
82 82 else
83 83 format.html { render :action => "edit" }
84 84 format.api { render_validation_errors(@group) }
85 85 end
86 86 end
87 87 end
88 88
89 89 def destroy
90 90 @group.destroy
91 91
92 92 respond_to do |format|
93 93 format.html { redirect_to(groups_path) }
94 94 format.api { render_api_ok }
95 95 end
96 96 end
97 97
98 98 def new_users
99 99 end
100 100
101 101 def add_users
102 102 @users = User.not_in_group(@group).where(:id => (params[:user_id] || params[:user_ids])).to_a
103 103 @group.users << @users
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 {
108 108 if @users.any?
109 109 render_api_ok
110 110 else
111 111 render_api_errors "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}"
112 112 end
113 113 }
114 114 end
115 115 end
116 116
117 117 def remove_user
118 118 @group.users.delete(User.find(params[:user_id])) if request.delete?
119 119 respond_to do |format|
120 120 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
121 121 format.js
122 122 format.api { render_api_ok }
123 123 end
124 124 end
125 125
126 126 def autocomplete_for_user
127 127 respond_to do |format|
128 128 format.js
129 129 end
130 130 end
131 131
132 132 private
133 133
134 134 def find_group
135 135 @group = Group.find(params[:id])
136 136 rescue ActiveRecord::RecordNotFound
137 137 render_404
138 138 end
139 139
140 140 def user_count_by_group_id
141 141 h = User.joins(:groups).group('group_id').count
142 142 h.keys.each do |key|
143 143 h[key.to_i] = h.delete(key)
144 144 end
145 145 h
146 146 end
147 147 end
@@ -1,122 +1,122
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueCategoriesController < ApplicationController
19 19 menu_item :settings
20 20 model_object IssueCategory
21 21 before_filter :find_model_object, :except => [:index, :new, :create]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
24 24 before_filter :authorize
25 25 accept_api_auth :index, :show, :create, :update, :destroy
26 26
27 27 def index
28 28 respond_to do |format|
29 29 format.html { redirect_to_settings_in_projects }
30 30 format.api { @categories = @project.issue_categories.to_a }
31 31 end
32 32 end
33 33
34 34 def show
35 35 respond_to do |format|
36 36 format.html { redirect_to_settings_in_projects }
37 37 format.api
38 38 end
39 39 end
40 40
41 41 def new
42 42 @category = @project.issue_categories.build
43 43 @category.safe_attributes = params[:issue_category]
44 44
45 45 respond_to do |format|
46 46 format.html
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def create
52 52 @category = @project.issue_categories.build
53 53 @category.safe_attributes = params[:issue_category]
54 54 if @category.save
55 55 respond_to do |format|
56 56 format.html do
57 57 flash[:notice] = l(:notice_successful_create)
58 58 redirect_to_settings_in_projects
59 59 end
60 60 format.js
61 61 format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
62 62 end
63 63 else
64 64 respond_to do |format|
65 65 format.html { render :action => 'new'}
66 66 format.js { render :action => 'new'}
67 67 format.api { render_validation_errors(@category) }
68 68 end
69 69 end
70 70 end
71 71
72 72 def edit
73 73 end
74 74
75 75 def update
76 76 @category.safe_attributes = params[:issue_category]
77 77 if @category.save
78 78 respond_to do |format|
79 79 format.html {
80 80 flash[:notice] = l(:notice_successful_update)
81 81 redirect_to_settings_in_projects
82 82 }
83 83 format.api { render_api_ok }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 format.html { render :action => 'edit' }
88 88 format.api { render_validation_errors(@category) }
89 89 end
90 90 end
91 91 end
92 92
93 93 def destroy
94 94 @issue_count = @category.issues.size
95 95 if @issue_count == 0 || params[:todo] || api_request?
96 96 reassign_to = nil
97 97 if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
98 98 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
99 99 end
100 100 @category.destroy(reassign_to)
101 101 respond_to do |format|
102 102 format.html { redirect_to_settings_in_projects }
103 103 format.api { render_api_ok }
104 104 end
105 105 return
106 106 end
107 107 @categories = @project.issue_categories - [@category]
108 108 end
109 109
110 110 private
111 111
112 112 def redirect_to_settings_in_projects
113 113 redirect_to settings_project_path(@project, :tab => 'categories')
114 114 end
115 115
116 116 # Wrap ApplicationController's find_model_object method to set
117 117 # @category instead of just @issue_category
118 118 def find_model_object
119 119 super
120 120 @category = @object
121 121 end
122 122 end
@@ -1,92 +1,92
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_issue, :authorize, :only => [:index, :create]
20 20 before_filter :find_relation, :only => [:show, :destroy]
21 21
22 22 accept_api_auth :index, :show, :create, :destroy
23 23
24 24 def index
25 25 @relations = @issue.relations
26 26
27 27 respond_to do |format|
28 28 format.html { render :nothing => true }
29 29 format.api
30 30 end
31 31 end
32 32
33 33 def show
34 34 raise Unauthorized unless @relation.visible?
35 35
36 36 respond_to do |format|
37 37 format.html { render :nothing => true }
38 38 format.api
39 39 end
40 40 end
41 41
42 42 def create
43 43 @relation = IssueRelation.new(params[:relation])
44 44 @relation.issue_from = @issue
45 45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
46 46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
47 47 end
48 48 @relation.init_journals(User.current)
49 49 saved = @relation.save
50 50
51 51 respond_to do |format|
52 52 format.html { redirect_to issue_path(@issue) }
53 53 format.js {
54 54 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
55 55 }
56 56 format.api {
57 57 if saved
58 58 render :action => 'show', :status => :created, :location => relation_url(@relation)
59 59 else
60 60 render_validation_errors(@relation)
61 61 end
62 62 }
63 63 end
64 64 end
65 65
66 66 def destroy
67 67 raise Unauthorized unless @relation.deletable?
68 68 @relation.init_journals(User.current)
69 69 @relation.destroy
70 70
71 71 respond_to do |format|
72 72 format.html { redirect_to issue_path(@relation.issue_from) }
73 73 format.js
74 74 format.api { render_api_ok }
75 75 end
76 76 end
77 77
78 78 private
79 79
80 80 def find_issue
81 81 @issue = Issue.find(params[:issue_id])
82 82 @project = @issue.project
83 83 rescue ActiveRecord::RecordNotFound
84 84 render_404
85 85 end
86 86
87 87 def find_relation
88 88 @relation = IssueRelation.find(params[:id])
89 89 rescue ActiveRecord::RecordNotFound
90 90 render_404
91 91 end
92 92 end
@@ -1,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueStatusesController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @issue_status_pages, @issue_statuses = paginate IssueStatus.sorted, :per_page => 25
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @issue_statuses = IssueStatus.order('position').to_a
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @issue_status = IssueStatus.new
39 39 end
40 40
41 41 def create
42 42 @issue_status = IssueStatus.new(params[:issue_status])
43 43 if @issue_status.save
44 44 flash[:notice] = l(:notice_successful_create)
45 45 redirect_to issue_statuses_path
46 46 else
47 47 render :action => 'new'
48 48 end
49 49 end
50 50
51 51 def edit
52 52 @issue_status = IssueStatus.find(params[:id])
53 53 end
54 54
55 55 def update
56 56 @issue_status = IssueStatus.find(params[:id])
57 57 if @issue_status.update_attributes(params[:issue_status])
58 58 flash[:notice] = l(:notice_successful_update)
59 59 redirect_to issue_statuses_path(:page => params[:page])
60 60 else
61 61 render :action => 'edit'
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 IssueStatus.find(params[:id]).destroy
67 67 redirect_to issue_statuses_path
68 68 rescue
69 69 flash[:error] = l(:error_unable_delete_issue_status)
70 70 redirect_to issue_statuses_path
71 71 end
72 72
73 73 def update_issue_done_ratio
74 74 if request.post? && IssueStatus.update_issue_done_ratios
75 75 flash[:notice] = l(:notice_issue_done_ratios_updated)
76 76 else
77 77 flash[:error] = l(:error_issue_done_ratios_not_updated)
78 78 end
79 79 redirect_to issue_statuses_path
80 80 end
81 81 end
@@ -1,502 +1,502
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 28 accept_rss_auth :index, :show
29 29 accept_api_auth :index, :show, :create, :update, :destroy
30 30
31 31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 32
33 33 helper :journals
34 34 helper :projects
35 35 include ProjectsHelper
36 36 helper :custom_fields
37 37 include CustomFieldsHelper
38 38 helper :issue_relations
39 39 include IssueRelationsHelper
40 40 helper :watchers
41 41 include WatchersHelper
42 42 helper :attachments
43 43 include AttachmentsHelper
44 44 helper :queries
45 45 include QueriesHelper
46 46 helper :repositories
47 47 include RepositoriesHelper
48 48 helper :sort
49 49 include SortHelper
50 50 include IssuesHelper
51 51 helper :timelog
52 52 include Redmine::Export::PDF
53 53
54 54 def index
55 55 retrieve_query
56 56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 57 sort_update(@query.sortable_columns)
58 58 @query.sort_criteria = sort_criteria.to_a
59 59
60 60 if @query.valid?
61 61 case params[:format]
62 62 when 'csv', 'pdf'
63 63 @limit = Setting.issues_export_limit.to_i
64 64 if params[:columns] == 'all'
65 65 @query.column_names = @query.available_inline_columns.map(&:name)
66 66 end
67 67 when 'atom'
68 68 @limit = Setting.feeds_limit.to_i
69 69 when 'xml', 'json'
70 70 @offset, @limit = api_offset_and_limit
71 71 @query.column_names = %w(author)
72 72 else
73 73 @limit = per_page_option
74 74 end
75 75
76 76 @issue_count = @query.issue_count
77 77 @issue_pages = Paginator.new @issue_count, @limit, params['page']
78 78 @offset ||= @issue_pages.offset
79 79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 80 :order => sort_clause,
81 81 :offset => @offset,
82 82 :limit => @limit)
83 83 @issue_count_by_group = @query.issue_count_by_group
84 84
85 85 respond_to do |format|
86 86 format.html { render :template => 'issues/index', :layout => !request.xhr? }
87 87 format.api {
88 88 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
89 89 }
90 90 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
91 91 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
92 92 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
93 93 end
94 94 else
95 95 respond_to do |format|
96 96 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
97 97 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
98 98 format.api { render_validation_errors(@query) }
99 99 end
100 100 end
101 101 rescue ActiveRecord::RecordNotFound
102 102 render_404
103 103 end
104 104
105 105 def show
106 106 @journals = @issue.journals.includes(:user, :details).
107 107 references(:user, :details).
108 108 reorder("#{Journal.table_name}.id ASC").to_a
109 109 @journals.each_with_index {|j,i| j.indice = i+1}
110 110 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
111 111 Journal.preload_journals_details_custom_fields(@journals)
112 112 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
113 113 @journals.reverse! if User.current.wants_comments_in_reverse_order?
114 114
115 115 @changesets = @issue.changesets.visible.to_a
116 116 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
117 117
118 118 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
119 119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
120 120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
121 121 @priorities = IssuePriority.active
122 122 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
123 123 @relation = IssueRelation.new
124 124
125 125 respond_to do |format|
126 126 format.html {
127 127 retrieve_previous_and_next_issue_ids
128 128 render :template => 'issues/show'
129 129 }
130 130 format.api
131 131 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
132 132 format.pdf {
133 133 pdf = issue_to_pdf(@issue, :journals => @journals)
134 134 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
135 135 }
136 136 end
137 137 end
138 138
139 139 # Add a new issue
140 140 # The new issue will be created from an existing one if copy_from parameter is given
141 141 def new
142 142 respond_to do |format|
143 143 format.html { render :action => 'new', :layout => !request.xhr? }
144 144 end
145 145 end
146 146
147 147 def create
148 148 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
149 149 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
150 150 if @issue.save
151 151 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
152 152 respond_to do |format|
153 153 format.html {
154 154 render_attachment_warning_if_needed(@issue)
155 155 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
156 156 if params[:continue]
157 157 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
158 158 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
159 159 else
160 160 redirect_to issue_path(@issue)
161 161 end
162 162 }
163 163 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
164 164 end
165 165 return
166 166 else
167 167 respond_to do |format|
168 168 format.html { render :action => 'new' }
169 169 format.api { render_validation_errors(@issue) }
170 170 end
171 171 end
172 172 end
173 173
174 174 def edit
175 175 return unless update_issue_from_params
176 176
177 177 respond_to do |format|
178 178 format.html { }
179 179 format.xml { }
180 180 end
181 181 end
182 182
183 183 def update
184 184 return unless update_issue_from_params
185 185 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
186 186 saved = false
187 187 begin
188 188 saved = save_issue_with_child_records
189 189 rescue ActiveRecord::StaleObjectError
190 190 @conflict = true
191 191 if params[:last_journal_id]
192 192 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
193 193 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
194 194 end
195 195 end
196 196
197 197 if saved
198 198 render_attachment_warning_if_needed(@issue)
199 199 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200 200
201 201 respond_to do |format|
202 202 format.html { redirect_back_or_default issue_path(@issue) }
203 203 format.api { render_api_ok }
204 204 end
205 205 else
206 206 respond_to do |format|
207 207 format.html { render :action => 'edit' }
208 208 format.api { render_validation_errors(@issue) }
209 209 end
210 210 end
211 211 end
212 212
213 213 # Updates the issue form when changing the project, status or tracker
214 214 # on issue creation/update
215 215 def update_form
216 216 end
217 217
218 218 # Bulk edit/copy a set of issues
219 219 def bulk_edit
220 220 @issues.sort!
221 221 @copy = params[:copy].present?
222 222 @notes = params[:notes]
223 223
224 224 if User.current.allowed_to?(:move_issues, @projects)
225 225 @allowed_projects = Issue.allowed_target_projects_on_move
226 226 if params[:issue]
227 227 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
228 228 if @target_project
229 229 target_projects = [@target_project]
230 230 end
231 231 end
232 232 end
233 233 target_projects ||= @projects
234 234
235 235 if @copy
236 236 # Copied issues will get their default statuses
237 237 @available_statuses = []
238 238 else
239 239 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
240 240 end
241 241 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
242 242 @assignables = target_projects.map(&:assignable_users).reduce(:&)
243 243 @trackers = target_projects.map(&:trackers).reduce(:&)
244 244 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
245 245 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
246 246 if @copy
247 247 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
248 248 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
249 249 end
250 250
251 251 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
252 252
253 253 @issue_params = params[:issue] || {}
254 254 @issue_params[:custom_field_values] ||= {}
255 255 end
256 256
257 257 def bulk_update
258 258 @issues.sort!
259 259 @copy = params[:copy].present?
260 260 attributes = parse_params_for_bulk_issue_attributes(params)
261 261
262 262 unsaved_issues = []
263 263 saved_issues = []
264 264
265 265 if @copy && params[:copy_subtasks].present?
266 266 # Descendant issues will be copied with the parent task
267 267 # Don't copy them twice
268 268 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
269 269 end
270 270
271 271 @issues.each do |orig_issue|
272 272 orig_issue.reload
273 273 if @copy
274 274 issue = orig_issue.copy({},
275 275 :attachments => params[:copy_attachments].present?,
276 276 :subtasks => params[:copy_subtasks].present?,
277 277 :link => link_copy?(params[:link_copy])
278 278 )
279 279 else
280 280 issue = orig_issue
281 281 end
282 282 journal = issue.init_journal(User.current, params[:notes])
283 283 issue.safe_attributes = attributes
284 284 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
285 285 if issue.save
286 286 saved_issues << issue
287 287 else
288 288 unsaved_issues << orig_issue
289 289 end
290 290 end
291 291
292 292 if unsaved_issues.empty?
293 293 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
294 294 if params[:follow]
295 295 if @issues.size == 1 && saved_issues.size == 1
296 296 redirect_to issue_path(saved_issues.first)
297 297 elsif saved_issues.map(&:project).uniq.size == 1
298 298 redirect_to project_issues_path(saved_issues.map(&:project).first)
299 299 end
300 300 else
301 301 redirect_back_or_default _project_issues_path(@project)
302 302 end
303 303 else
304 304 @saved_issues = @issues
305 305 @unsaved_issues = unsaved_issues
306 306 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
307 307 bulk_edit
308 308 render :action => 'bulk_edit'
309 309 end
310 310 end
311 311
312 312 def destroy
313 313 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
314 314 if @hours > 0
315 315 case params[:todo]
316 316 when 'destroy'
317 317 # nothing to do
318 318 when 'nullify'
319 319 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
320 320 when 'reassign'
321 321 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
322 322 if reassign_to.nil?
323 323 flash.now[:error] = l(:error_issue_not_found_in_project)
324 324 return
325 325 else
326 326 TimeEntry.where(['issue_id IN (?)', @issues]).
327 327 update_all("issue_id = #{reassign_to.id}")
328 328 end
329 329 else
330 330 # display the destroy form if it's a user request
331 331 return unless api_request?
332 332 end
333 333 end
334 334 @issues.each do |issue|
335 335 begin
336 336 issue.reload.destroy
337 337 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
338 338 # nothing to do, issue was already deleted (eg. by a parent)
339 339 end
340 340 end
341 341 respond_to do |format|
342 342 format.html { redirect_back_or_default _project_issues_path(@project) }
343 343 format.api { render_api_ok }
344 344 end
345 345 end
346 346
347 347 private
348 348
349 349 def find_project
350 350 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
351 351 @project = Project.find(project_id)
352 352 rescue ActiveRecord::RecordNotFound
353 353 render_404
354 354 end
355 355
356 356 def retrieve_previous_and_next_issue_ids
357 357 retrieve_query_from_session
358 358 if @query
359 359 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
360 360 sort_update(@query.sortable_columns, 'issues_index_sort')
361 361 limit = 500
362 362 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
363 363 if (idx = issue_ids.index(@issue.id)) && idx < limit
364 364 if issue_ids.size < 500
365 365 @issue_position = idx + 1
366 366 @issue_count = issue_ids.size
367 367 end
368 368 @prev_issue_id = issue_ids[idx - 1] if idx > 0
369 369 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
370 370 end
371 371 end
372 372 end
373 373
374 374 # Used by #edit and #update to set some common instance variables
375 375 # from the params
376 376 # TODO: Refactor, not everything in here is needed by #edit
377 377 def update_issue_from_params
378 378 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
379 379 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
380 380 if params[:time_entry]
381 381 @time_entry.attributes = params[:time_entry]
382 382 end
383 383
384 384 @issue.init_journal(User.current)
385 385
386 386 issue_attributes = params[:issue]
387 387 if issue_attributes && params[:conflict_resolution]
388 388 case params[:conflict_resolution]
389 389 when 'overwrite'
390 390 issue_attributes = issue_attributes.dup
391 391 issue_attributes.delete(:lock_version)
392 392 when 'add_notes'
393 393 issue_attributes = issue_attributes.slice(:notes)
394 394 when 'cancel'
395 395 redirect_to issue_path(@issue)
396 396 return false
397 397 end
398 398 end
399 399 @issue.safe_attributes = issue_attributes
400 400 @priorities = IssuePriority.active
401 401 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
402 402 true
403 403 end
404 404
405 405 # TODO: Refactor, lots of extra code in here
406 406 # TODO: Changing tracker on an existing issue should not trigger this
407 407 def build_new_issue_from_params
408 408 if params[:id].blank?
409 409 @issue = Issue.new
410 410 if params[:copy_from]
411 411 begin
412 412 @issue.init_journal(User.current)
413 413 @copy_from = Issue.visible.find(params[:copy_from])
414 414 @link_copy = link_copy?(params[:link_copy]) || request.get?
415 415 @copy_attachments = params[:copy_attachments].present? || request.get?
416 416 @copy_subtasks = params[:copy_subtasks].present? || request.get?
417 417 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
418 418 rescue ActiveRecord::RecordNotFound
419 419 render_404
420 420 return
421 421 end
422 422 end
423 423 @issue.project = @project
424 424 @issue.author ||= User.current
425 425 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
426 426 else
427 427 @issue = @project.issues.visible.find(params[:id])
428 428 end
429 429
430 430 if attrs = params[:issue].deep_dup
431 431 if params[:was_default_status] == attrs[:status_id]
432 432 attrs.delete(:status_id)
433 433 end
434 434 @issue.safe_attributes = attrs
435 435 end
436 436 @issue.tracker ||= @project.trackers.first
437 437 if @issue.tracker.nil?
438 438 render_error l(:error_no_tracker_in_project)
439 439 return false
440 440 end
441 441 if @issue.status.nil?
442 442 render_error l(:error_no_default_issue_status)
443 443 return false
444 444 end
445 445
446 446 @priorities = IssuePriority.active
447 447 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
448 448 @available_watchers = @issue.watcher_users
449 449 if @issue.project.users.count <= 20
450 450 @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
451 451 end
452 452 end
453 453
454 454 def parse_params_for_bulk_issue_attributes(params)
455 455 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
456 456 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
457 457 if custom = attributes[:custom_field_values]
458 458 custom.reject! {|k,v| v.blank?}
459 459 custom.keys.each do |k|
460 460 if custom[k].is_a?(Array)
461 461 custom[k] << '' if custom[k].delete('__none__')
462 462 else
463 463 custom[k] = '' if custom[k] == '__none__'
464 464 end
465 465 end
466 466 end
467 467 attributes
468 468 end
469 469
470 470 # Saves @issue and a time_entry from the parameters
471 471 def save_issue_with_child_records
472 472 Issue.transaction do
473 473 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
474 474 time_entry = @time_entry || TimeEntry.new
475 475 time_entry.project = @issue.project
476 476 time_entry.issue = @issue
477 477 time_entry.user = User.current
478 478 time_entry.spent_on = User.current.today
479 479 time_entry.attributes = params[:time_entry]
480 480 @issue.time_entries << time_entry
481 481 end
482 482
483 483 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
484 484 if @issue.save
485 485 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
486 486 else
487 487 raise ActiveRecord::Rollback
488 488 end
489 489 end
490 490 end
491 491
492 492 def link_copy?(param)
493 493 case Setting.link_copied_issue
494 494 when 'yes'
495 495 true
496 496 when 'no'
497 497 false
498 498 when 'ask'
499 499 param == '1'
500 500 end
501 501 end
502 502 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class JournalsController < ApplicationController
19 19 before_filter :find_journal, :only => [:edit, :diff]
20 20 before_filter :find_issue, :only => [:new]
21 21 before_filter :find_optional_project, :only => [:index]
22 22 before_filter :authorize, :only => [:new, :edit, :diff]
23 23 accept_rss_auth :index
24 24 menu_item :issues
25 25
26 26 helper :issues
27 27 helper :custom_fields
28 28 helper :queries
29 29 include QueriesHelper
30 30 helper :sort
31 31 include SortHelper
32 32
33 33 def index
34 34 retrieve_query
35 35 sort_init 'id', 'desc'
36 36 sort_update(@query.sortable_columns)
37 37 if @query.valid?
38 38 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
39 39 :limit => 25)
40 40 end
41 41 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
42 42 render :layout => false, :content_type => 'application/atom+xml'
43 43 rescue ActiveRecord::RecordNotFound
44 44 render_404
45 45 end
46 46
47 47 def diff
48 48 @issue = @journal.issue
49 49 if params[:detail_id].present?
50 50 @detail = @journal.details.find_by_id(params[:detail_id])
51 51 else
52 52 @detail = @journal.details.detect {|d| d.prop_key == 'description'}
53 53 end
54 54 (render_404; return false) unless @issue && @detail
55 55 @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
56 56 end
57 57
58 58 def new
59 59 @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
60 60 if @journal
61 61 user = @journal.user
62 62 text = @journal.notes
63 63 else
64 64 user = @issue.author
65 65 text = @issue.description
66 66 end
67 67 # Replaces pre blocks with [...]
68 68 text = text.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]')
69 69 @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
70 70 @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
71 71 rescue ActiveRecord::RecordNotFound
72 72 render_404
73 73 end
74 74
75 75 def edit
76 76 (render_403; return false) unless @journal.editable_by?(User.current)
77 77 if request.post?
78 78 @journal.update_attributes(:notes => params[:notes]) if params[:notes]
79 79 @journal.destroy if @journal.details.empty? && @journal.notes.blank?
80 80 call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
81 81 respond_to do |format|
82 82 format.html { redirect_to issue_path(@journal.journalized) }
83 83 format.js { render :action => 'update' }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 # TODO: implement non-JS journal update
88 88 format.js
89 89 end
90 90 end
91 91 end
92 92
93 93 private
94 94
95 95 def find_journal
96 96 @journal = Journal.visible.find(params[:id])
97 97 @project = @journal.journalized.project
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,131 +1,131
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MembersController < ApplicationController
19 19 model_object Member
20 20 before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete]
23 23 before_filter :authorize
24 24 accept_api_auth :index, :show, :create, :update, :destroy
25 25
26 26 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 to_a
36 36 respond_to do |format|
37 37 format.html { head 406 }
38 38 format.api
39 39 end
40 40 end
41 41
42 42 def show
43 43 respond_to do |format|
44 44 format.html { head 406 }
45 45 format.api
46 46 end
47 47 end
48 48
49 49 def new
50 50 @member = Member.new
51 51 end
52 52
53 53 def create
54 54 members = []
55 55 if params[:membership]
56 56 if params[:membership][:user_ids]
57 57 attrs = params[:membership].dup
58 58 user_ids = attrs.delete(:user_ids)
59 59 user_ids.each do |user_id|
60 60 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
61 61 end
62 62 else
63 63 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
64 64 end
65 65 @project.members << members
66 66 end
67 67
68 68 respond_to do |format|
69 69 format.html { redirect_to_settings_in_projects }
70 70 format.js {
71 71 @members = members
72 72 @member = Member.new
73 73 }
74 74 format.api {
75 75 @member = members.first
76 76 if @member.valid?
77 77 render :action => 'show', :status => :created, :location => membership_url(@member)
78 78 else
79 79 render_validation_errors(@member)
80 80 end
81 81 }
82 82 end
83 83 end
84 84
85 85 def update
86 86 if params[:membership]
87 87 @member.role_ids = params[:membership][:role_ids]
88 88 end
89 89 saved = @member.save
90 90 respond_to do |format|
91 91 format.html { redirect_to_settings_in_projects }
92 92 format.js
93 93 format.api {
94 94 if saved
95 95 render_api_ok
96 96 else
97 97 render_validation_errors(@member)
98 98 end
99 99 }
100 100 end
101 101 end
102 102
103 103 def destroy
104 104 if request.delete? && @member.deletable?
105 105 @member.destroy
106 106 end
107 107 respond_to do |format|
108 108 format.html { redirect_to_settings_in_projects }
109 109 format.js
110 110 format.api {
111 111 if @member.destroyed?
112 112 render_api_ok
113 113 else
114 114 head :unprocessable_entity
115 115 end
116 116 }
117 117 end
118 118 end
119 119
120 120 def autocomplete
121 121 respond_to do |format|
122 122 format.js
123 123 end
124 124 end
125 125
126 126 private
127 127
128 128 def redirect_to_settings_in_projects
129 129 redirect_to settings_project_path(@project, :tab => 'members')
130 130 end
131 131 end
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MessagesController < ApplicationController
19 19 menu_item :boards
20 20 default_search_scope :messages
21 21 before_filter :find_board, :only => [:new, :preview]
22 22 before_filter :find_attachments, :only => [:preview]
23 23 before_filter :find_message, :except => [:new, :preview]
24 24 before_filter :authorize, :except => [:preview, :edit, :destroy]
25 25
26 26 helper :boards
27 27 helper :watchers
28 28 helper :attachments
29 29 include AttachmentsHelper
30 30
31 31 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
32 32
33 33 # Show a topic and its replies
34 34 def show
35 35 page = params[:page]
36 36 # Find the page of the requested reply
37 37 if params[:r] && page.nil?
38 38 offset = @topic.children.where("#{Message.table_name}.id < ?", params[:r].to_i).count
39 39 page = 1 + offset / REPLIES_PER_PAGE
40 40 end
41 41
42 42 @reply_count = @topic.children.count
43 43 @reply_pages = Paginator.new @reply_count, REPLIES_PER_PAGE, page
44 44 @replies = @topic.children.
45 45 includes(:author, :attachments, {:board => :project}).
46 46 reorder("#{Message.table_name}.created_on ASC, #{Message.table_name}.id ASC").
47 47 limit(@reply_pages.per_page).
48 48 offset(@reply_pages.offset).
49 49 to_a
50 50
51 51 @reply = Message.new(:subject => "RE: #{@message.subject}")
52 52 render :action => "show", :layout => false if request.xhr?
53 53 end
54 54
55 55 # Create a new topic
56 56 def new
57 57 @message = Message.new
58 58 @message.author = User.current
59 59 @message.board = @board
60 60 @message.safe_attributes = params[:message]
61 61 if request.post?
62 62 @message.save_attachments(params[:attachments])
63 63 if @message.save
64 64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
65 65 render_attachment_warning_if_needed(@message)
66 66 redirect_to board_message_path(@board, @message)
67 67 end
68 68 end
69 69 end
70 70
71 71 # Reply to a topic
72 72 def reply
73 73 @reply = Message.new
74 74 @reply.author = User.current
75 75 @reply.board = @board
76 76 @reply.safe_attributes = params[:reply]
77 77 @topic.children << @reply
78 78 if !@reply.new_record?
79 79 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
80 80 attachments = Attachment.attach_files(@reply, params[:attachments])
81 81 render_attachment_warning_if_needed(@reply)
82 82 end
83 83 redirect_to board_message_path(@board, @topic, :r => @reply)
84 84 end
85 85
86 86 # Edit a message
87 87 def edit
88 88 (render_403; return false) unless @message.editable_by?(User.current)
89 89 @message.safe_attributes = params[:message]
90 90 if request.post? && @message.save
91 91 attachments = Attachment.attach_files(@message, params[:attachments])
92 92 render_attachment_warning_if_needed(@message)
93 93 flash[:notice] = l(:notice_successful_update)
94 94 @message.reload
95 95 redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id))
96 96 end
97 97 end
98 98
99 99 # Delete a messages
100 100 def destroy
101 101 (render_403; return false) unless @message.destroyable_by?(User.current)
102 102 r = @message.to_param
103 103 @message.destroy
104 104 if @message.parent
105 105 redirect_to board_message_path(@board, @message.parent, :r => r)
106 106 else
107 107 redirect_to project_board_path(@project, @board)
108 108 end
109 109 end
110 110
111 111 def quote
112 112 @subject = @message.subject
113 113 @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:')
114 114
115 115 @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> "
116 116 @content << @message.content.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
117 117 end
118 118
119 119 def preview
120 120 message = @board.messages.find_by_id(params[:id])
121 121 @text = (params[:message] || params[:reply])[:content]
122 122 @previewed = message
123 123 render :partial => 'common/preview'
124 124 end
125 125
126 126 private
127 127 def find_message
128 128 return unless find_board
129 129 @message = @board.messages.includes(:parent).find(params[:id])
130 130 @topic = @message.root
131 131 rescue ActiveRecord::RecordNotFound
132 132 render_404
133 133 end
134 134
135 135 def find_board
136 136 @board = Board.includes(:project).find(params[:board_id])
137 137 @project = @board.project
138 138 rescue ActiveRecord::RecordNotFound
139 139 render_404
140 140 nil
141 141 end
142 142 end
@@ -1,204 +1,204
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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] if params[:user]
57 57 @user.pref.attributes = params[:pref] if 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 # Reset the session creation time to not log out this session on next
104 104 # request due to ApplicationController#force_logout_if_password_changed
105 105 session[:ctime] = Time.now.utc.to_i
106 106 flash[:notice] = l(:notice_account_password_updated)
107 107 redirect_to my_account_path
108 108 end
109 109 end
110 110 end
111 111 end
112 112
113 113 # Create a new feeds key
114 114 def reset_rss_key
115 115 if request.post?
116 116 if User.current.rss_token
117 117 User.current.rss_token.destroy
118 118 User.current.reload
119 119 end
120 120 User.current.rss_key
121 121 flash[:notice] = l(:notice_feeds_access_key_reseted)
122 122 end
123 123 redirect_to my_account_path
124 124 end
125 125
126 126 # Create a new API key
127 127 def reset_api_key
128 128 if request.post?
129 129 if User.current.api_token
130 130 User.current.api_token.destroy
131 131 User.current.reload
132 132 end
133 133 User.current.api_key
134 134 flash[:notice] = l(:notice_api_access_key_reseted)
135 135 end
136 136 redirect_to my_account_path
137 137 end
138 138
139 139 # User's page layout configuration
140 140 def page_layout
141 141 @user = User.current
142 142 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
143 143 @block_options = []
144 144 BLOCKS.each do |k, v|
145 145 unless @blocks.values.flatten.include?(k)
146 146 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
147 147 end
148 148 end
149 149 end
150 150
151 151 # Add a block to user's page
152 152 # The block is added on top of the page
153 153 # params[:block] : id of the block to add
154 154 def add_block
155 155 block = params[:block].to_s.underscore
156 156 if block.present? && BLOCKS.key?(block)
157 157 @user = User.current
158 158 layout = @user.pref[:my_page_layout] || {}
159 159 # remove if already present in a group
160 160 %w(top left right).each {|f| (layout[f] ||= []).delete block }
161 161 # add it on top
162 162 layout['top'].unshift block
163 163 @user.pref[:my_page_layout] = layout
164 164 @user.pref.save
165 165 end
166 166 redirect_to my_page_layout_path
167 167 end
168 168
169 169 # Remove a block to user's page
170 170 # params[:block] : id of the block to remove
171 171 def remove_block
172 172 block = params[:block].to_s.underscore
173 173 @user = User.current
174 174 # remove block in all groups
175 175 layout = @user.pref[:my_page_layout] || {}
176 176 %w(top left right).each {|f| (layout[f] ||= []).delete block }
177 177 @user.pref[:my_page_layout] = layout
178 178 @user.pref.save
179 179 redirect_to my_page_layout_path
180 180 end
181 181
182 182 # Change blocks order on user's page
183 183 # params[:group] : group to order (top, left or right)
184 184 # params[:list-(top|left|right)] : array of block ids of the group
185 185 def order_blocks
186 186 group = params[:group]
187 187 @user = User.current
188 188 if group.is_a?(String)
189 189 group_items = (params["blocks"] || []).collect(&:underscore)
190 190 group_items.each {|s| s.sub!(/^block_/, '')}
191 191 if group_items and group_items.is_a? Array
192 192 layout = @user.pref[:my_page_layout] || {}
193 193 # remove group blocks if they are presents in other groups
194 194 %w(top left right).each {|f|
195 195 layout[f] = (layout[f] || []) - group_items
196 196 }
197 197 layout[group] = group_items
198 198 @user.pref[:my_page_layout] = layout
199 199 @user.pref.save
200 200 end
201 201 end
202 202 render :nothing => true
203 203 end
204 204 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class NewsController < ApplicationController
19 19 default_search_scope :news
20 20 model_object News
21 21 before_filter :find_model_object, :except => [:new, :create, :index]
22 22 before_filter :find_project_from_association, :except => [:new, :create, :index]
23 23 before_filter :find_project_by_project_id, :only => [:new, :create]
24 24 before_filter :authorize, :except => [:index]
25 25 before_filter :find_optional_project, :only => :index
26 26 accept_rss_auth :index
27 27 accept_api_auth :index
28 28
29 29 helper :watchers
30 30 helper :attachments
31 31
32 32 def index
33 33 case params[:format]
34 34 when 'xml', 'json'
35 35 @offset, @limit = api_offset_and_limit
36 36 else
37 37 @limit = 10
38 38 end
39 39
40 40 scope = @project ? @project.news.visible : News.visible
41 41
42 42 @news_count = scope.count
43 43 @news_pages = Paginator.new @news_count, @limit, params['page']
44 44 @offset ||= @news_pages.offset
45 45 @newss = scope.includes([:author, :project]).
46 46 order("#{News.table_name}.created_on DESC").
47 47 limit(@limit).
48 48 offset(@offset).
49 49 to_a
50 50 respond_to do |format|
51 51 format.html {
52 52 @news = News.new # for adding news inline
53 53 render :layout => false if request.xhr?
54 54 }
55 55 format.api
56 56 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
57 57 end
58 58 end
59 59
60 60 def show
61 61 @comments = @news.comments.to_a
62 62 @comments.reverse! if User.current.wants_comments_in_reverse_order?
63 63 end
64 64
65 65 def new
66 66 @news = News.new(:project => @project, :author => User.current)
67 67 end
68 68
69 69 def create
70 70 @news = News.new(:project => @project, :author => User.current)
71 71 @news.safe_attributes = params[:news]
72 72 @news.save_attachments(params[:attachments])
73 73 if @news.save
74 74 render_attachment_warning_if_needed(@news)
75 75 flash[:notice] = l(:notice_successful_create)
76 76 redirect_to project_news_index_path(@project)
77 77 else
78 78 render :action => 'new'
79 79 end
80 80 end
81 81
82 82 def edit
83 83 end
84 84
85 85 def update
86 86 @news.safe_attributes = params[:news]
87 87 @news.save_attachments(params[:attachments])
88 88 if @news.save
89 89 render_attachment_warning_if_needed(@news)
90 90 flash[:notice] = l(:notice_successful_update)
91 91 redirect_to news_path(@news)
92 92 else
93 93 render :action => 'edit'
94 94 end
95 95 end
96 96
97 97 def destroy
98 98 @news.destroy
99 99 redirect_to project_news_index_path(@project)
100 100 end
101 101
102 102 private
103 103
104 104 def find_optional_project
105 105 return true unless params[:project_id]
106 106 @project = Project.find(params[:project_id])
107 107 authorize
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class PreviewsController < ApplicationController
19 19 before_filter :find_project, :find_attachments
20 20
21 21 def issue
22 22 @issue = Issue.visible.find_by_id(params[:id]) unless params[:id].blank?
23 23 if @issue
24 24 @description = params[:issue] && params[:issue][:description]
25 25 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
26 26 @description = nil
27 27 end
28 28 # params[:notes] is useful for preview of notes in issue history
29 29 @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil)
30 30 else
31 31 @description = (params[:issue] ? params[:issue][:description] : nil)
32 32 end
33 33 render :layout => false
34 34 end
35 35
36 36 def news
37 37 if params[:id].present? && news = News.visible.find_by_id(params[:id])
38 38 @previewed = news
39 39 end
40 40 @text = (params[:news] ? params[:news][:description] : nil)
41 41 render :partial => 'common/preview'
42 42 end
43 43
44 44 private
45 45
46 46 def find_project
47 47 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
48 48 @project = Project.find(project_id)
49 49 rescue ActiveRecord::RecordNotFound
50 50 render_404
51 51 end
52 52
53 53 end
@@ -1,80 +1,80
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class PrincipalMembershipsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_principal, :only => [:new, :create]
23 23 before_filter :find_membership, :only => [:update, :destroy]
24 24
25 25 def new
26 26 @projects = Project.active.all
27 27 @roles = Role.find_all_givable
28 28 respond_to do |format|
29 29 format.html
30 30 format.js
31 31 end
32 32 end
33 33
34 34 def create
35 35 @members = Member.create_principal_memberships(@principal, params[:membership])
36 36 respond_to do |format|
37 37 format.html { redirect_to_principal @principal }
38 38 format.js
39 39 end
40 40 end
41 41
42 42 def update
43 43 @membership.attributes = params[:membership]
44 44 @membership.save
45 45 respond_to do |format|
46 46 format.html { redirect_to_principal @principal }
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def destroy
52 52 if @membership.deletable?
53 53 @membership.destroy
54 54 end
55 55 respond_to do |format|
56 56 format.html { redirect_to_principal @principal }
57 57 format.js
58 58 end
59 59 end
60 60
61 61 private
62 62
63 63 def find_principal
64 64 principal_id = params[:user_id] || params[:group_id]
65 65 @principal = Principal.find(principal_id)
66 66 rescue ActiveRecord::RecordNotFound
67 67 render_404
68 68 end
69 69
70 70 def find_membership
71 71 @membership = Member.find(params[:id])
72 72 @principal = @membership.principal
73 73 rescue ActiveRecord::RecordNotFound
74 74 render_404
75 75 end
76 76
77 77 def redirect_to_principal(principal)
78 78 redirect_to edit_polymorphic_path(principal, :tab => 'memberships')
79 79 end
80 80 end
@@ -1,42 +1,42
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectEnumerationsController < ApplicationController
19 19 before_filter :find_project_by_project_id
20 20 before_filter :authorize
21 21
22 22 def update
23 23 if params[:enumerations]
24 24 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,233 +1,233
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :settings, :only => :settings
21 21
22 22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 24 before_filter :authorize_global, :only => [:new, :create]
25 25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 26 accept_rss_auth :index
27 27 accept_api_auth :index, :show, :create, :update, :destroy
28 28
29 29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 30 if controller.request.post?
31 31 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 32 end
33 33 end
34 34
35 35 helper :custom_fields
36 36 helper :issues
37 37 helper :queries
38 38 helper :repositories
39 39 helper :members
40 40
41 41 # Lists visible projects
42 42 def index
43 43 scope = Project.visible.sorted
44 44
45 45 respond_to do |format|
46 46 format.html {
47 47 unless params[:closed]
48 48 scope = scope.active
49 49 end
50 50 @projects = scope.to_a
51 51 }
52 52 format.api {
53 53 @offset, @limit = api_offset_and_limit
54 54 @project_count = scope.count
55 55 @projects = scope.offset(@offset).limit(@limit).to_a
56 56 }
57 57 format.atom {
58 58 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
59 59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 60 }
61 61 end
62 62 end
63 63
64 64 def new
65 65 @issue_custom_fields = IssueCustomField.sorted.to_a
66 66 @trackers = Tracker.sorted.to_a
67 67 @project = Project.new
68 68 @project.safe_attributes = params[:project]
69 69 end
70 70
71 71 def create
72 72 @issue_custom_fields = IssueCustomField.sorted.to_a
73 73 @trackers = Tracker.sorted.to_a
74 74 @project = Project.new
75 75 @project.safe_attributes = params[:project]
76 76
77 77 if @project.save
78 78 unless User.current.admin?
79 79 @project.add_default_member(User.current)
80 80 end
81 81 respond_to do |format|
82 82 format.html {
83 83 flash[:notice] = l(:notice_successful_create)
84 84 if params[:continue]
85 85 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
86 86 redirect_to new_project_path(attrs)
87 87 else
88 88 redirect_to settings_project_path(@project)
89 89 end
90 90 }
91 91 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 92 end
93 93 else
94 94 respond_to do |format|
95 95 format.html { render :action => 'new' }
96 96 format.api { render_validation_errors(@project) }
97 97 end
98 98 end
99 99 end
100 100
101 101 def copy
102 102 @issue_custom_fields = IssueCustomField.sorted.to_a
103 103 @trackers = Tracker.sorted.to_a
104 104 @source_project = Project.find(params[:id])
105 105 if request.get?
106 106 @project = Project.copy_from(@source_project)
107 107 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
108 108 else
109 109 Mailer.with_deliveries(params[:notifications] == '1') do
110 110 @project = Project.new
111 111 @project.safe_attributes = params[:project]
112 112 if @project.copy(@source_project, :only => params[:only])
113 113 flash[:notice] = l(:notice_successful_create)
114 114 redirect_to settings_project_path(@project)
115 115 elsif !@project.new_record?
116 116 # Project was created
117 117 # But some objects were not copied due to validation failures
118 118 # (eg. issues from disabled trackers)
119 119 # TODO: inform about that
120 120 redirect_to settings_project_path(@project)
121 121 end
122 122 end
123 123 end
124 124 rescue ActiveRecord::RecordNotFound
125 125 # source_project not found
126 126 render_404
127 127 end
128 128
129 129 # Show @project
130 130 def show
131 131 # try to redirect to the requested menu item
132 132 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
133 133 return
134 134 end
135 135
136 136 @users_by_role = @project.users_by_role
137 137 @subprojects = @project.children.visible.to_a
138 138 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
139 139 @trackers = @project.rolled_up_trackers
140 140
141 141 cond = @project.project_condition(Setting.display_subprojects_issues?)
142 142
143 143 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
144 144 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
145 145
146 146 if User.current.allowed_to?(:view_time_entries, @project)
147 147 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
148 148 end
149 149
150 150 @key = User.current.rss_key
151 151
152 152 respond_to do |format|
153 153 format.html
154 154 format.api
155 155 end
156 156 end
157 157
158 158 def settings
159 159 @issue_custom_fields = IssueCustomField.sorted.to_a
160 160 @issue_category ||= IssueCategory.new
161 161 @member ||= @project.members.new
162 162 @trackers = Tracker.sorted.to_a
163 163 @wiki ||= @project.wiki || Wiki.new(:project => @project)
164 164 end
165 165
166 166 def edit
167 167 end
168 168
169 169 def update
170 170 @project.safe_attributes = params[:project]
171 171 if @project.save
172 172 respond_to do |format|
173 173 format.html {
174 174 flash[:notice] = l(:notice_successful_update)
175 175 redirect_to settings_project_path(@project)
176 176 }
177 177 format.api { render_api_ok }
178 178 end
179 179 else
180 180 respond_to do |format|
181 181 format.html {
182 182 settings
183 183 render :action => 'settings'
184 184 }
185 185 format.api { render_validation_errors(@project) }
186 186 end
187 187 end
188 188 end
189 189
190 190 def modules
191 191 @project.enabled_module_names = params[:enabled_module_names]
192 192 flash[:notice] = l(:notice_successful_update)
193 193 redirect_to settings_project_path(@project, :tab => 'modules')
194 194 end
195 195
196 196 def archive
197 197 unless @project.archive
198 198 flash[:error] = l(:error_can_not_archive_project)
199 199 end
200 200 redirect_to admin_projects_path(:status => params[:status])
201 201 end
202 202
203 203 def unarchive
204 204 unless @project.active?
205 205 @project.unarchive
206 206 end
207 207 redirect_to admin_projects_path(:status => params[:status])
208 208 end
209 209
210 210 def close
211 211 @project.close
212 212 redirect_to project_path(@project)
213 213 end
214 214
215 215 def reopen
216 216 @project.reopen
217 217 redirect_to project_path(@project)
218 218 end
219 219
220 220 # Delete @project
221 221 def destroy
222 222 @project_to_destroy = @project
223 223 if api_request? || params[:confirm]
224 224 @project_to_destroy.destroy
225 225 respond_to do |format|
226 226 format.html { redirect_to admin_projects_path }
227 227 format.api { render_api_ok }
228 228 end
229 229 end
230 230 # hide project in layout
231 231 @project = nil
232 232 end
233 233 end
@@ -1,121 +1,121
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => [:new, :create, :index]
21 21 before_filter :find_optional_project, :only => [:new, :create]
22 22
23 23 accept_api_auth :index
24 24
25 25 include QueriesHelper
26 26
27 27 def index
28 28 case params[:format]
29 29 when 'xml', 'json'
30 30 @offset, @limit = api_offset_and_limit
31 31 else
32 32 @limit = per_page_option
33 33 end
34 34 @query_count = IssueQuery.visible.count
35 35 @query_pages = Paginator.new @query_count, @limit, params['page']
36 36 @queries = IssueQuery.visible.
37 37 order("#{Query.table_name}.name").
38 38 limit(@limit).
39 39 offset(@offset).
40 40 to_a
41 41 respond_to do |format|
42 42 format.html {render_error :status => 406}
43 43 format.api
44 44 end
45 45 end
46 46
47 47 def new
48 48 @query = IssueQuery.new
49 49 @query.user = User.current
50 50 @query.project = @project
51 51 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
52 52 @query.build_from_params(params)
53 53 end
54 54
55 55 def create
56 56 @query = IssueQuery.new(params[:query])
57 57 @query.user = User.current
58 58 @query.project = params[:query_is_for_all] ? nil : @project
59 59 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
60 60 @query.build_from_params(params)
61 61 @query.column_names = nil if params[:default_columns]
62 62
63 63 if @query.save
64 64 flash[:notice] = l(:notice_successful_create)
65 65 redirect_to_issues(:query_id => @query)
66 66 else
67 67 render :action => 'new', :layout => !request.xhr?
68 68 end
69 69 end
70 70
71 71 def edit
72 72 end
73 73
74 74 def update
75 75 @query.attributes = params[:query]
76 76 @query.project = nil if params[:query_is_for_all]
77 77 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
78 78 @query.build_from_params(params)
79 79 @query.column_names = nil if params[:default_columns]
80 80
81 81 if @query.save
82 82 flash[:notice] = l(:notice_successful_update)
83 83 redirect_to_issues(:query_id => @query)
84 84 else
85 85 render :action => 'edit'
86 86 end
87 87 end
88 88
89 89 def destroy
90 90 @query.destroy
91 91 redirect_to_issues(:set_filter => 1)
92 92 end
93 93
94 94 private
95 95 def find_query
96 96 @query = IssueQuery.find(params[:id])
97 97 @project = @query.project
98 98 render_403 unless @query.editable_by?(User.current)
99 99 rescue ActiveRecord::RecordNotFound
100 100 render_404
101 101 end
102 102
103 103 def find_optional_project
104 104 @project = Project.find(params[:project_id]) if params[:project_id]
105 105 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
106 106 rescue ActiveRecord::RecordNotFound
107 107 render_404
108 108 end
109 109
110 110 def redirect_to_issues(options)
111 111 if params[:gantt]
112 112 if @project
113 113 redirect_to project_gantt_path(@project, options)
114 114 else
115 115 redirect_to issues_gantt_path(options)
116 116 end
117 117 else
118 118 redirect_to _project_issues_path(@project, options)
119 119 end
120 120 end
121 121 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ReportsController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :authorize, :find_issue_statuses
21 21
22 22 def issue_report
23 23 @trackers = @project.trackers
24 24 @versions = @project.shared_versions.sort
25 25 @priorities = IssuePriority.all.reverse
26 26 @categories = @project.issue_categories
27 27 @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
28 28 @authors = @project.users.sort
29 29 @subprojects = @project.descendants.visible
30 30
31 31 @issues_by_tracker = Issue.by_tracker(@project)
32 32 @issues_by_version = Issue.by_version(@project)
33 33 @issues_by_priority = Issue.by_priority(@project)
34 34 @issues_by_category = Issue.by_category(@project)
35 35 @issues_by_assigned_to = Issue.by_assigned_to(@project)
36 36 @issues_by_author = Issue.by_author(@project)
37 37 @issues_by_subproject = Issue.by_subproject(@project) || []
38 38
39 39 render :template => "reports/issue_report"
40 40 end
41 41
42 42 def issue_report_details
43 43 case params[:detail]
44 44 when "tracker"
45 45 @field = "tracker_id"
46 46 @rows = @project.trackers
47 47 @data = Issue.by_tracker(@project)
48 48 @report_title = l(:field_tracker)
49 49 when "version"
50 50 @field = "fixed_version_id"
51 51 @rows = @project.shared_versions.sort
52 52 @data = Issue.by_version(@project)
53 53 @report_title = l(:field_version)
54 54 when "priority"
55 55 @field = "priority_id"
56 56 @rows = IssuePriority.all.reverse
57 57 @data = Issue.by_priority(@project)
58 58 @report_title = l(:field_priority)
59 59 when "category"
60 60 @field = "category_id"
61 61 @rows = @project.issue_categories
62 62 @data = Issue.by_category(@project)
63 63 @report_title = l(:field_category)
64 64 when "assigned_to"
65 65 @field = "assigned_to_id"
66 66 @rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
67 67 @data = Issue.by_assigned_to(@project)
68 68 @report_title = l(:field_assigned_to)
69 69 when "author"
70 70 @field = "author_id"
71 71 @rows = @project.users.sort
72 72 @data = Issue.by_author(@project)
73 73 @report_title = l(:field_author)
74 74 when "subproject"
75 75 @field = "project_id"
76 76 @rows = @project.descendants.visible
77 77 @data = Issue.by_subproject(@project) || []
78 78 @report_title = l(:field_subproject)
79 79 end
80 80
81 81 respond_to do |format|
82 82 if @field
83 83 format.html {}
84 84 else
85 85 format.html { redirect_to :action => 'issue_report', :id => @project }
86 86 end
87 87 end
88 88 end
89 89
90 90 private
91 91
92 92 def find_issue_statuses
93 93 @statuses = IssueStatus.sorted.to_a
94 94 end
95 95 end
@@ -1,439 +1,439
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21 require 'redmine/scm/adapters'
22 22
23 23 class ChangesetNotFound < Exception; end
24 24 class InvalidRevisionParam < Exception; end
25 25
26 26 class RepositoriesController < ApplicationController
27 27 menu_item :repository
28 28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 29 default_search_scope :changesets
30 30
31 31 before_filter :find_project_by_project_id, :only => [:new, :create]
32 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 35 before_filter :authorize
36 36 accept_rss_auth :revisions
37 37
38 38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39 39
40 40 def new
41 41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
42 42 @repository = Repository.factory(scm)
43 43 @repository.is_default = @project.repository.nil?
44 44 @repository.project = @project
45 45 end
46 46
47 47 def create
48 48 attrs = pickup_extra_info
49 49 @repository = Repository.factory(params[:repository_scm])
50 50 @repository.safe_attributes = params[:repository]
51 51 if attrs[:attrs_extra].keys.any?
52 52 @repository.merge_extra_info(attrs[:attrs_extra])
53 53 end
54 54 @repository.project = @project
55 55 if request.post? && @repository.save
56 56 redirect_to settings_project_path(@project, :tab => 'repositories')
57 57 else
58 58 render :action => 'new'
59 59 end
60 60 end
61 61
62 62 def edit
63 63 end
64 64
65 65 def update
66 66 attrs = pickup_extra_info
67 67 @repository.safe_attributes = attrs[:attrs]
68 68 if attrs[:attrs_extra].keys.any?
69 69 @repository.merge_extra_info(attrs[:attrs_extra])
70 70 end
71 71 @repository.project = @project
72 72 if @repository.save
73 73 redirect_to settings_project_path(@project, :tab => 'repositories')
74 74 else
75 75 render :action => 'edit'
76 76 end
77 77 end
78 78
79 79 def pickup_extra_info
80 80 p = {}
81 81 p_extra = {}
82 82 params[:repository].each do |k, v|
83 83 if k =~ /^extra_/
84 84 p_extra[k] = v
85 85 else
86 86 p[k] = v
87 87 end
88 88 end
89 89 {:attrs => p, :attrs_extra => p_extra}
90 90 end
91 91 private :pickup_extra_info
92 92
93 93 def committers
94 94 @committers = @repository.committers
95 95 @users = @project.users
96 96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 97 @users += User.where(:id => additional_user_ids).to_a unless additional_user_ids.empty?
98 98 @users.compact!
99 99 @users.sort!
100 100 if request.post? && params[:committers].is_a?(Hash)
101 101 # Build a hash with repository usernames as keys and corresponding user ids as values
102 102 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
103 103 flash[:notice] = l(:notice_successful_update)
104 104 redirect_to settings_project_path(@project, :tab => 'repositories')
105 105 end
106 106 end
107 107
108 108 def destroy
109 109 @repository.destroy if request.delete?
110 110 redirect_to settings_project_path(@project, :tab => 'repositories')
111 111 end
112 112
113 113 def show
114 114 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
115 115
116 116 @entries = @repository.entries(@path, @rev)
117 117 @changeset = @repository.find_changeset_by_name(@rev)
118 118 if request.xhr?
119 119 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
120 120 else
121 121 (show_error_not_found; return) unless @entries
122 122 @changesets = @repository.latest_changesets(@path, @rev)
123 123 @properties = @repository.properties(@path, @rev)
124 124 @repositories = @project.repositories
125 125 render :action => 'show'
126 126 end
127 127 end
128 128
129 129 alias_method :browse, :show
130 130
131 131 def changes
132 132 @entry = @repository.entry(@path, @rev)
133 133 (show_error_not_found; return) unless @entry
134 134 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
135 135 @properties = @repository.properties(@path, @rev)
136 136 @changeset = @repository.find_changeset_by_name(@rev)
137 137 end
138 138
139 139 def revisions
140 140 @changeset_count = @repository.changesets.count
141 141 @changeset_pages = Paginator.new @changeset_count,
142 142 per_page_option,
143 143 params['page']
144 144 @changesets = @repository.changesets.
145 145 limit(@changeset_pages.per_page).
146 146 offset(@changeset_pages.offset).
147 147 includes(:user, :repository, :parents).
148 148 to_a
149 149
150 150 respond_to do |format|
151 151 format.html { render :layout => false if request.xhr? }
152 152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
153 153 end
154 154 end
155 155
156 156 def raw
157 157 entry_and_raw(true)
158 158 end
159 159
160 160 def entry
161 161 entry_and_raw(false)
162 162 end
163 163
164 164 def entry_and_raw(is_raw)
165 165 @entry = @repository.entry(@path, @rev)
166 166 (show_error_not_found; return) unless @entry
167 167
168 168 # If the entry is a dir, show the browser
169 169 (show; return) if @entry.is_dir?
170 170
171 171 @content = @repository.cat(@path, @rev)
172 172 (show_error_not_found; return) unless @content
173 173 if is_raw ||
174 174 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
175 175 ! is_entry_text_data?(@content, @path)
176 176 # Force the download
177 177 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
178 178 send_type = Redmine::MimeType.of(@path)
179 179 send_opt[:type] = send_type.to_s if send_type
180 180 send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
181 181 send_data @content, send_opt
182 182 else
183 183 # Prevent empty lines when displaying a file with Windows style eol
184 184 # TODO: UTF-16
185 185 # Is this needs? AttachmentsController reads file simply.
186 186 @content.gsub!("\r\n", "\n")
187 187 @changeset = @repository.find_changeset_by_name(@rev)
188 188 end
189 189 end
190 190 private :entry_and_raw
191 191
192 192 def is_entry_text_data?(ent, path)
193 193 # UTF-16 contains "\x00".
194 194 # It is very strict that file contains less than 30% of ascii symbols
195 195 # in non Western Europe.
196 196 return true if Redmine::MimeType.is_type?('text', path)
197 197 # Ruby 1.8.6 has a bug of integer divisions.
198 198 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
199 199 return false if ent.is_binary_data?
200 200 true
201 201 end
202 202 private :is_entry_text_data?
203 203
204 204 def annotate
205 205 @entry = @repository.entry(@path, @rev)
206 206 (show_error_not_found; return) unless @entry
207 207
208 208 @annotate = @repository.scm.annotate(@path, @rev)
209 209 if @annotate.nil? || @annotate.empty?
210 210 (render_error l(:error_scm_annotate); return)
211 211 end
212 212 ann_buf_size = 0
213 213 @annotate.lines.each do |buf|
214 214 ann_buf_size += buf.size
215 215 end
216 216 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
217 217 (render_error l(:error_scm_annotate_big_text_file); return)
218 218 end
219 219 @changeset = @repository.find_changeset_by_name(@rev)
220 220 end
221 221
222 222 def revision
223 223 respond_to do |format|
224 224 format.html
225 225 format.js {render :layout => false}
226 226 end
227 227 end
228 228
229 229 # Adds a related issue to a changeset
230 230 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
231 231 def add_related_issue
232 232 issue_id = params[:issue_id].to_s.sub(/^#/,'')
233 233 @issue = @changeset.find_referenced_issue_by_id(issue_id)
234 234 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
235 235 @issue = nil
236 236 end
237 237
238 238 if @issue
239 239 @changeset.issues << @issue
240 240 end
241 241 end
242 242
243 243 # Removes a related issue from a changeset
244 244 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
245 245 def remove_related_issue
246 246 @issue = Issue.visible.find_by_id(params[:issue_id])
247 247 if @issue
248 248 @changeset.issues.delete(@issue)
249 249 end
250 250 end
251 251
252 252 def diff
253 253 if params[:format] == 'diff'
254 254 @diff = @repository.diff(@path, @rev, @rev_to)
255 255 (show_error_not_found; return) unless @diff
256 256 filename = "changeset_r#{@rev}"
257 257 filename << "_r#{@rev_to}" if @rev_to
258 258 send_data @diff.join, :filename => "#{filename}.diff",
259 259 :type => 'text/x-patch',
260 260 :disposition => 'attachment'
261 261 else
262 262 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
263 263 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
264 264
265 265 # Save diff type as user preference
266 266 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
267 267 User.current.pref[:diff_type] = @diff_type
268 268 User.current.preference.save
269 269 end
270 270 @cache_key = "repositories/diff/#{@repository.id}/" +
271 271 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
272 272 unless read_fragment(@cache_key)
273 273 @diff = @repository.diff(@path, @rev, @rev_to)
274 274 show_error_not_found unless @diff
275 275 end
276 276
277 277 @changeset = @repository.find_changeset_by_name(@rev)
278 278 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
279 279 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
280 280 end
281 281 end
282 282
283 283 def stats
284 284 end
285 285
286 286 def graph
287 287 data = nil
288 288 case params[:graph]
289 289 when "commits_per_month"
290 290 data = graph_commits_per_month(@repository)
291 291 when "commits_per_author"
292 292 data = graph_commits_per_author(@repository)
293 293 end
294 294 if data
295 295 headers["Content-Type"] = "image/svg+xml"
296 296 send_data(data, :type => "image/svg+xml", :disposition => "inline")
297 297 else
298 298 render_404
299 299 end
300 300 end
301 301
302 302 private
303 303
304 304 def find_repository
305 305 @repository = Repository.find(params[:id])
306 306 @project = @repository.project
307 307 rescue ActiveRecord::RecordNotFound
308 308 render_404
309 309 end
310 310
311 311 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
312 312
313 313 def find_project_repository
314 314 @project = Project.find(params[:id])
315 315 if params[:repository_id].present?
316 316 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
317 317 else
318 318 @repository = @project.repository
319 319 end
320 320 (render_404; return false) unless @repository
321 321 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
322 322 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
323 323 @rev_to = params[:rev_to]
324 324
325 325 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
326 326 if @repository.branches.blank?
327 327 raise InvalidRevisionParam
328 328 end
329 329 end
330 330 rescue ActiveRecord::RecordNotFound
331 331 render_404
332 332 rescue InvalidRevisionParam
333 333 show_error_not_found
334 334 end
335 335
336 336 def find_changeset
337 337 if @rev.present?
338 338 @changeset = @repository.find_changeset_by_name(@rev)
339 339 end
340 340 show_error_not_found unless @changeset
341 341 end
342 342
343 343 def show_error_not_found
344 344 render_error :message => l(:error_scm_not_found), :status => 404
345 345 end
346 346
347 347 # Handler for Redmine::Scm::Adapters::CommandFailed exception
348 348 def show_error_command_failed(exception)
349 349 render_error l(:error_scm_command_failed, exception.message)
350 350 end
351 351
352 352 def graph_commits_per_month(repository)
353 353 @date_to = Date.today
354 354 @date_from = @date_to << 11
355 355 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
356 356 commits_by_day = Changeset.
357 357 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
358 358 group(:commit_date).
359 359 count
360 360 commits_by_month = [0] * 12
361 361 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
362 362
363 363 changes_by_day = Change.
364 364 joins(:changeset).
365 365 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
366 366 group(:commit_date).
367 367 count
368 368 changes_by_month = [0] * 12
369 369 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
370 370
371 371 fields = []
372 372 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
373 373
374 374 graph = SVG::Graph::Bar.new(
375 375 :height => 300,
376 376 :width => 800,
377 377 :fields => fields.reverse,
378 378 :stack => :side,
379 379 :scale_integers => true,
380 380 :step_x_labels => 2,
381 381 :show_data_values => false,
382 382 :graph_title => l(:label_commits_per_month),
383 383 :show_graph_title => true
384 384 )
385 385
386 386 graph.add_data(
387 387 :data => commits_by_month[0..11].reverse,
388 388 :title => l(:label_revision_plural)
389 389 )
390 390
391 391 graph.add_data(
392 392 :data => changes_by_month[0..11].reverse,
393 393 :title => l(:label_change_plural)
394 394 )
395 395
396 396 graph.burn
397 397 end
398 398
399 399 def graph_commits_per_author(repository)
400 400 #data
401 401 stats = repository.stats_by_author
402 402 fields, commits_data, changes_data = [], [], []
403 403 stats.each do |name, hsh|
404 404 fields << name
405 405 commits_data << hsh[:commits_count]
406 406 changes_data << hsh[:changes_count]
407 407 end
408 408
409 409 #expand to 10 values if needed
410 410 fields = fields + [""]*(10 - fields.length) if fields.length<10
411 411 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
412 412 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
413 413
414 414 # Remove email address in usernames
415 415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
416 416
417 417 #prepare graph
418 418 graph = SVG::Graph::BarHorizontal.new(
419 419 :height => 30 * commits_data.length,
420 420 :width => 800,
421 421 :fields => fields,
422 422 :stack => :side,
423 423 :scale_integers => true,
424 424 :show_data_values => false,
425 425 :rotate_y_labels => false,
426 426 :graph_title => l(:label_commits_per_author),
427 427 :show_graph_title => true
428 428 )
429 429 graph.add_data(
430 430 :data => commits_data,
431 431 :title => l(:label_revision_plural)
432 432 )
433 433 graph.add_data(
434 434 :data => changes_data,
435 435 :title => l(:label_change_plural)
436 436 )
437 437 graph.burn
438 438 end
439 439 end
@@ -1,108 +1,108
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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.to_a
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.to_a
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.to_a
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 @role.update_attributes(params[:role])
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to roles_path(:page => params[:page])
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.to_a
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,85 +1,85
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 21 def index
22 22 @question = params[:q] || ""
23 23 @question.strip!
24 24 @all_words = params[:all_words] ? params[:all_words].present? : true
25 25 @titles_only = params[:titles_only] ? params[:titles_only].present? : false
26 26 @search_attachments = params[:attachments].presence || '0'
27 27 @open_issues = params[:open_issues] ? params[:open_issues].present? : false
28 28
29 29 # quick jump to an issue
30 30 if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
31 31 redirect_to issue_path(issue)
32 32 return
33 33 end
34 34
35 35 projects_to_search =
36 36 case params[:scope]
37 37 when 'all'
38 38 nil
39 39 when 'my_projects'
40 40 User.current.projects
41 41 when 'subprojects'
42 42 @project ? (@project.self_and_descendants.active.to_a) : nil
43 43 else
44 44 @project
45 45 end
46 46
47 47 @object_types = Redmine::Search.available_search_types.dup
48 48 if projects_to_search.is_a? Project
49 49 # don't search projects
50 50 @object_types.delete('projects')
51 51 # only show what the user is allowed to view
52 52 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
53 53 end
54 54
55 55 @scope = @object_types.select {|t| params[t]}
56 56 @scope = @object_types if @scope.empty?
57 57
58 58 fetcher = Redmine::Search::Fetcher.new(
59 59 @question, User.current, @scope, projects_to_search,
60 60 :all_words => @all_words, :titles_only => @titles_only, :attachments => @search_attachments, :open_issues => @open_issues,
61 61 :cache => params[:page].present?
62 62 )
63 63
64 64 if fetcher.tokens.present?
65 65 @result_count = fetcher.result_count
66 66 @result_count_by_type = fetcher.result_count_by_type
67 67 @tokens = fetcher.tokens
68 68
69 69 @result_pages = Paginator.new @result_count, 10, params['page']
70 70 @results = fetcher.results(@result_pages.offset, @result_pages.per_page)
71 71 else
72 72 @question = ""
73 73 end
74 74 render :layout => false if request.xhr?
75 75 end
76 76
77 77 private
78 78 def find_optional_project
79 79 return true unless params[:id]
80 80 @project = Project.find(params[:id])
81 81 check_project_privacy
82 82 rescue ActiveRecord::RecordNotFound
83 83 render_404
84 84 end
85 85 end
@@ -1,74 +1,74
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SysController < ActionController::Base
19 19 before_filter :check_enabled
20 20
21 21 def projects
22 22 p = Project.active.has_module(:repository).
23 23 order("#{Project.table_name}.identifier").preload(:repository).to_a
24 24 # extra_info attribute from repository breaks activeresource client
25 25 render :xml => p.to_xml(
26 26 :only => [:id, :identifier, :name, :is_public, :status],
27 27 :include => {:repository => {:only => [:id, :url]}}
28 28 )
29 29 end
30 30
31 31 def create_project_repository
32 32 project = Project.find(params[:id])
33 33 if project.repository
34 34 render :nothing => true, :status => 409
35 35 else
36 36 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
37 37 repository = Repository.factory(params[:vendor], params[:repository])
38 38 repository.project = project
39 39 if repository.save
40 40 render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
41 41 else
42 42 render :nothing => true, :status => 422
43 43 end
44 44 end
45 45 end
46 46
47 47 def fetch_changesets
48 48 projects = []
49 49 scope = Project.active.has_module(:repository)
50 50 if params[:id]
51 51 project = nil
52 52 if params[:id].to_s =~ /^\d*$/
53 53 project = scope.find(params[:id])
54 54 else
55 55 project = scope.find_by_identifier(params[:id])
56 56 end
57 57 raise ActiveRecord::RecordNotFound unless project
58 58 projects << project
59 59 else
60 60 projects = scope.to_a
61 61 end
62 62 projects.each do |project|
63 63 project.repositories.each do |repository|
64 64 repository.fetch_changesets
65 65 end
66 66 end
67 67 render :nothing => true, :status => 200
68 68 rescue ActiveRecord::RecordNotFound
69 69 render :nothing => true, :status => 404
70 70 end
71 71
72 72 protected
73 73
74 74 def check_enabled
75 75 User.current = nil
76 76 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
77 77 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
78 78 return false
79 79 end
80 80 end
81 81 end
@@ -1,280 +1,280
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 20
21 21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24 24
25 25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
26 26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27 27
28 28 accept_rss_auth :index
29 29 accept_api_auth :index, :show, :create, :update, :destroy
30 30
31 31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 32
33 33 helper :sort
34 34 include SortHelper
35 35 helper :issues
36 36 include TimelogHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :queries
40 40 include QueriesHelper
41 41
42 42 def index
43 43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
44 44
45 45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
46 46 sort_update(@query.sortable_columns)
47 47 scope = time_entry_scope(:order => sort_clause).
48 48 includes(:project, :user, :issue).
49 49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
50 50
51 51 respond_to do |format|
52 52 format.html {
53 53 @entry_count = scope.count
54 54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
56 56 @total_hours = scope.sum(:hours).to_f
57 57
58 58 render :layout => !request.xhr?
59 59 }
60 60 format.api {
61 61 @entry_count = scope.count
62 62 @offset, @limit = api_offset_and_limit
63 63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
64 64 }
65 65 format.atom {
66 66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
67 67 render_feed(entries, :title => l(:label_spent_time))
68 68 }
69 69 format.csv {
70 70 # Export all entries
71 71 @entries = scope.to_a
72 72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
73 73 }
74 74 end
75 75 end
76 76
77 77 def report
78 78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
79 79 scope = time_entry_scope
80 80
81 81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
82 82
83 83 respond_to do |format|
84 84 format.html { render :layout => !request.xhr? }
85 85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
86 86 end
87 87 end
88 88
89 89 def show
90 90 respond_to do |format|
91 91 # TODO: Implement html response
92 92 format.html { render :nothing => true, :status => 406 }
93 93 format.api
94 94 end
95 95 end
96 96
97 97 def new
98 98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
99 99 @time_entry.safe_attributes = params[:time_entry]
100 100 end
101 101
102 102 def create
103 103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
104 104 @time_entry.safe_attributes = params[:time_entry]
105 105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 106 render_403
107 107 return
108 108 end
109 109
110 110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
111 111
112 112 if @time_entry.save
113 113 respond_to do |format|
114 114 format.html {
115 115 flash[:notice] = l(:notice_successful_create)
116 116 if params[:continue]
117 117 options = {
118 118 :time_entry => {
119 119 :project_id => params[:time_entry][:project_id],
120 120 :issue_id => @time_entry.issue_id,
121 121 :activity_id => @time_entry.activity_id
122 122 },
123 123 :back_url => params[:back_url]
124 124 }
125 125 if params[:project_id] && @time_entry.project
126 126 redirect_to new_project_time_entry_path(@time_entry.project, options)
127 127 elsif params[:issue_id] && @time_entry.issue
128 128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
129 129 else
130 130 redirect_to new_time_entry_path(options)
131 131 end
132 132 else
133 133 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 134 end
135 135 }
136 136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 137 end
138 138 else
139 139 respond_to do |format|
140 140 format.html { render :action => 'new' }
141 141 format.api { render_validation_errors(@time_entry) }
142 142 end
143 143 end
144 144 end
145 145
146 146 def edit
147 147 @time_entry.safe_attributes = params[:time_entry]
148 148 end
149 149
150 150 def update
151 151 @time_entry.safe_attributes = params[:time_entry]
152 152
153 153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154 154
155 155 if @time_entry.save
156 156 respond_to do |format|
157 157 format.html {
158 158 flash[:notice] = l(:notice_successful_update)
159 159 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 160 }
161 161 format.api { render_api_ok }
162 162 end
163 163 else
164 164 respond_to do |format|
165 165 format.html { render :action => 'edit' }
166 166 format.api { render_validation_errors(@time_entry) }
167 167 end
168 168 end
169 169 end
170 170
171 171 def bulk_edit
172 172 @available_activities = TimeEntryActivity.shared.active
173 173 @custom_fields = TimeEntry.first.available_custom_fields
174 174 end
175 175
176 176 def bulk_update
177 177 attributes = parse_params_for_bulk_time_entry_attributes(params)
178 178
179 179 unsaved_time_entry_ids = []
180 180 @time_entries.each do |time_entry|
181 181 time_entry.reload
182 182 time_entry.safe_attributes = attributes
183 183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 184 unless time_entry.save
185 185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
186 186 # Keep unsaved time_entry ids to display them in flash error
187 187 unsaved_time_entry_ids << time_entry.id
188 188 end
189 189 end
190 190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 191 redirect_back_or_default project_time_entries_path(@projects.first)
192 192 end
193 193
194 194 def destroy
195 195 destroyed = TimeEntry.transaction do
196 196 @time_entries.each do |t|
197 197 unless t.destroy && t.destroyed?
198 198 raise ActiveRecord::Rollback
199 199 end
200 200 end
201 201 end
202 202
203 203 respond_to do |format|
204 204 format.html {
205 205 if destroyed
206 206 flash[:notice] = l(:notice_successful_delete)
207 207 else
208 208 flash[:error] = l(:notice_unable_delete_time_entry)
209 209 end
210 210 redirect_back_or_default project_time_entries_path(@projects.first)
211 211 }
212 212 format.api {
213 213 if destroyed
214 214 render_api_ok
215 215 else
216 216 render_validation_errors(@time_entries)
217 217 end
218 218 }
219 219 end
220 220 end
221 221
222 222 private
223 223 def find_time_entry
224 224 @time_entry = TimeEntry.find(params[:id])
225 225 unless @time_entry.editable_by?(User.current)
226 226 render_403
227 227 return false
228 228 end
229 229 @project = @time_entry.project
230 230 rescue ActiveRecord::RecordNotFound
231 231 render_404
232 232 end
233 233
234 234 def find_time_entries
235 235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
236 236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 237 @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
255 255 if params[:issue_id].present?
256 256 @issue = Issue.find(params[:issue_id])
257 257 @project = @issue.project
258 258 elsif params[:project_id].present?
259 259 @project = Project.find(params[:project_id])
260 260 end
261 261 rescue ActiveRecord::RecordNotFound
262 262 render_404
263 263 end
264 264
265 265 # Returns the TimeEntry scope for index and report actions
266 266 def time_entry_scope(options={})
267 267 scope = @query.results_scope(options)
268 268 if @issue
269 269 scope = scope.on_issue(@issue)
270 270 end
271 271 scope
272 272 end
273 273
274 274 def parse_params_for_bulk_time_entry_attributes(params)
275 275 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
276 276 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
277 277 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
278 278 attributes
279 279 end
280 280 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TrackersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @tracker_pages, @trackers = paginate Tracker.sorted, :per_page => 25
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @trackers = Tracker.sorted.to_a
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @tracker ||= Tracker.new(params[:tracker])
39 39 @trackers = Tracker.sorted.to_a
40 40 @projects = Project.all
41 41 end
42 42
43 43 def create
44 44 @tracker = Tracker.new(params[:tracker])
45 45 if @tracker.save
46 46 # workflow copy
47 47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 48 @tracker.workflow_rules.copy(copy_from)
49 49 end
50 50 flash[:notice] = l(:notice_successful_create)
51 51 redirect_to trackers_path
52 52 return
53 53 end
54 54 new
55 55 render :action => 'new'
56 56 end
57 57
58 58 def edit
59 59 @tracker ||= Tracker.find(params[:id])
60 60 @projects = Project.all
61 61 end
62 62
63 63 def update
64 64 @tracker = Tracker.find(params[:id])
65 65 if @tracker.update_attributes(params[:tracker])
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to trackers_path(:page => params[:page])
68 68 return
69 69 end
70 70 edit
71 71 render :action => 'edit'
72 72 end
73 73
74 74 def destroy
75 75 @tracker = Tracker.find(params[:id])
76 76 unless @tracker.issues.empty?
77 77 flash[:error] = l(:error_can_not_delete_tracker)
78 78 else
79 79 @tracker.destroy
80 80 end
81 81 redirect_to trackers_path
82 82 end
83 83
84 84 def fields
85 85 if request.post? && params[:trackers]
86 86 params[:trackers].each do |tracker_id, tracker_params|
87 87 tracker = Tracker.find_by_id(tracker_id)
88 88 if tracker
89 89 tracker.core_fields = tracker_params[:core_fields]
90 90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
91 91 tracker.save
92 92 end
93 93 end
94 94 flash[:notice] = l(:notice_successful_update)
95 95 redirect_to fields_trackers_path
96 96 return
97 97 end
98 98 @trackers = Tracker.sorted.to_a
99 99 @custom_fields = IssueCustomField.all.sort
100 100 end
101 101 end
@@ -1,188 +1,188
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy]
23 23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29 helper :principal_memberships
30 30
31 31 def index
32 32 sort_init 'login', 'asc'
33 33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34 34
35 35 case params[:format]
36 36 when 'xml', 'json'
37 37 @offset, @limit = api_offset_and_limit
38 38 else
39 39 @limit = per_page_option
40 40 end
41 41
42 42 @status = params[:status] || 1
43 43
44 44 scope = User.logged.status(@status)
45 45 scope = scope.like(params[:name]) if params[:name].present?
46 46 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
47 47
48 48 @user_count = scope.count
49 49 @user_pages = Paginator.new @user_count, @limit, params['page']
50 50 @offset ||= @user_pages.offset
51 51 @users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a
52 52
53 53 respond_to do |format|
54 54 format.html {
55 55 @groups = Group.all.sort
56 56 render :layout => !request.xhr?
57 57 }
58 58 format.api
59 59 end
60 60 end
61 61
62 62 def show
63 63 unless @user.visible?
64 64 render_404
65 65 return
66 66 end
67 67
68 68 # show projects based on current user visibility
69 69 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
70 70
71 71 respond_to do |format|
72 72 format.html {
73 73 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
74 74 @events_by_day = events.group_by(&:event_date)
75 75 render :layout => 'base'
76 76 }
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] if 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] if 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 private
177 177
178 178 def find_user
179 179 if params[:id] == 'current'
180 180 require_login || return
181 181 @user = User.current
182 182 else
183 183 @user = User.find(params[:id])
184 184 end
185 185 rescue ActiveRecord::RecordNotFound
186 186 render_404
187 187 end
188 188 end
@@ -1,182 +1,182
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 model_object Version
21 21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
24 24 before_filter :authorize
25 25
26 26 accept_api_auth :index, :show, :create, :update, :destroy
27 27
28 28 helper :custom_fields
29 29 helper :projects
30 30
31 31 def index
32 32 respond_to do |format|
33 33 format.html {
34 34 @trackers = @project.trackers.sorted.to_a
35 35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38 38
39 39 @versions = @project.shared_versions || []
40 40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 41 @versions = @versions.uniq.sort
42 42 unless params[:completed]
43 43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 44 @versions -= @completed_versions
45 45 end
46 46
47 47 @issues_by_version = {}
48 48 if @selected_tracker_ids.any? && @versions.any?
49 49 issues = Issue.visible.
50 50 includes(:project, :tracker).
51 51 preload(:status, :priority, :fixed_version).
52 52 where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
53 53 order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
54 54 @issues_by_version = issues.group_by(&:fixed_version)
55 55 end
56 56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 57 }
58 58 format.api {
59 59 @versions = @project.shared_versions.to_a
60 60 }
61 61 end
62 62 end
63 63
64 64 def show
65 65 respond_to do |format|
66 66 format.html {
67 67 @issues = @version.fixed_issues.visible.
68 68 includes(:status, :tracker, :priority).
69 69 reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
70 70 to_a
71 71 }
72 72 format.api
73 73 end
74 74 end
75 75
76 76 def new
77 77 @version = @project.versions.build
78 78 @version.safe_attributes = params[:version]
79 79
80 80 respond_to do |format|
81 81 format.html
82 82 format.js
83 83 end
84 84 end
85 85
86 86 def create
87 87 @version = @project.versions.build
88 88 if params[:version]
89 89 attributes = params[:version].dup
90 90 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
91 91 @version.safe_attributes = attributes
92 92 end
93 93
94 94 if request.post?
95 95 if @version.save
96 96 respond_to do |format|
97 97 format.html do
98 98 flash[:notice] = l(:notice_successful_create)
99 99 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
100 100 end
101 101 format.js
102 102 format.api do
103 103 render :action => 'show', :status => :created, :location => version_url(@version)
104 104 end
105 105 end
106 106 else
107 107 respond_to do |format|
108 108 format.html { render :action => 'new' }
109 109 format.js { render :action => 'new' }
110 110 format.api { render_validation_errors(@version) }
111 111 end
112 112 end
113 113 end
114 114 end
115 115
116 116 def edit
117 117 end
118 118
119 119 def update
120 120 if params[:version]
121 121 attributes = params[:version].dup
122 122 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
123 123 @version.safe_attributes = attributes
124 124 if @version.save
125 125 respond_to do |format|
126 126 format.html {
127 127 flash[:notice] = l(:notice_successful_update)
128 128 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
129 129 }
130 130 format.api { render_api_ok }
131 131 end
132 132 else
133 133 respond_to do |format|
134 134 format.html { render :action => 'edit' }
135 135 format.api { render_validation_errors(@version) }
136 136 end
137 137 end
138 138 end
139 139 end
140 140
141 141 def close_completed
142 142 if request.put?
143 143 @project.close_completed_versions
144 144 end
145 145 redirect_to settings_project_path(@project, :tab => 'versions')
146 146 end
147 147
148 148 def destroy
149 149 if @version.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,131 +1,131
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WatchersController < ApplicationController
19 19 before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
20 20
21 21 def watch
22 22 set_watcher(@watchables, User.current, true)
23 23 end
24 24
25 25 def unwatch
26 26 set_watcher(@watchables, User.current, false)
27 27 end
28 28
29 29 before_filter :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
30 30 accept_api_auth :create, :destroy
31 31
32 32 def new
33 33 @users = users_for_new_watcher
34 34 end
35 35
36 36 def create
37 37 user_ids = []
38 38 if params[:watcher].is_a?(Hash)
39 39 user_ids << (params[:watcher][:user_ids] || params[:watcher][:user_id])
40 40 else
41 41 user_ids << params[:user_id]
42 42 end
43 43 users = User.active.visible.where(:id => user_ids.flatten.compact.uniq)
44 44 users.each do |user|
45 45 Watcher.create(:watchable => @watched, :user => user)
46 46 end
47 47 respond_to do |format|
48 48 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
49 49 format.js { @users = users_for_new_watcher }
50 50 format.api { render_api_ok }
51 51 end
52 52 end
53 53
54 54 def append
55 55 if params[:watcher].is_a?(Hash)
56 56 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
57 57 @users = User.active.visible.where(:id => user_ids).to_a
58 58 end
59 59 if @users.blank?
60 60 render :nothing => true
61 61 end
62 62 end
63 63
64 64 def destroy
65 65 @watched.set_watcher(User.visible.find(params[:user_id]), false)
66 66 respond_to do |format|
67 67 format.html { redirect_to :back }
68 68 format.js
69 69 format.api { render_api_ok }
70 70 end
71 71 end
72 72
73 73 def autocomplete_for_user
74 74 @users = users_for_new_watcher
75 75 render :layout => false
76 76 end
77 77
78 78 private
79 79
80 80 def find_project
81 81 if params[:object_type] && params[:object_id]
82 82 klass = Object.const_get(params[:object_type].camelcase)
83 83 return false unless klass.respond_to?('watched_by')
84 84 @watched = klass.find(params[:object_id])
85 85 @project = @watched.project
86 86 elsif params[:project_id]
87 87 @project = Project.visible.find_by_param(params[:project_id])
88 88 end
89 89 rescue
90 90 render_404
91 91 end
92 92
93 93 def find_watchables
94 94 klass = Object.const_get(params[:object_type].camelcase) rescue nil
95 95 if klass && klass.respond_to?('watched_by')
96 96 @watchables = klass.where(:id => Array.wrap(params[:object_id])).to_a
97 97 raise Unauthorized if @watchables.any? {|w|
98 98 if w.respond_to?(:visible?)
99 99 !w.visible?
100 100 elsif w.respond_to?(:project) && w.project
101 101 !w.project.visible?
102 102 end
103 103 }
104 104 end
105 105 render_404 unless @watchables.present?
106 106 end
107 107
108 108 def set_watcher(watchables, user, watching)
109 109 watchables.each do |watchable|
110 110 watchable.set_watcher(user, watching)
111 111 end
112 112 respond_to do |format|
113 113 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
114 114 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
115 115 end
116 116 end
117 117
118 118 def users_for_new_watcher
119 119 scope = nil
120 120 if params[:q].blank? && @project.present?
121 121 scope = @project.users
122 122 else
123 123 scope = User.all.limit(100)
124 124 end
125 125 users = scope.active.visible.sorted.like(params[:q]).to_a
126 126 if @watched
127 127 users -= @watched.watcher_users
128 128 end
129 129 users
130 130 end
131 131 end
@@ -1,30 +1,30
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,369 +1,369
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 end
189 189
190 190 # rename a page
191 191 def rename
192 192 return render_403 unless editable?
193 193 @page.redirect_existing_links = true
194 194 # used to display the *original* title if some AR validation errors occur
195 195 @original_title = @page.pretty_title
196 196 @page.safe_attributes = params[:wiki_page]
197 197 if request.post? && @page.save
198 198 flash[:notice] = l(:notice_successful_update)
199 199 redirect_to project_wiki_page_path(@page.project, @page.title)
200 200 end
201 201 end
202 202
203 203 def protect
204 204 @page.update_attribute :protected, params[:protected]
205 205 redirect_to project_wiki_page_path(@project, @page.title)
206 206 end
207 207
208 208 # show page history
209 209 def history
210 210 @version_count = @page.content.versions.count
211 211 @version_pages = Paginator.new @version_count, per_page_option, params['page']
212 212 # don't load text
213 213 @versions = @page.content.versions.
214 214 select("id, author_id, comments, updated_on, version").
215 215 reorder('version DESC').
216 216 limit(@version_pages.per_page + 1).
217 217 offset(@version_pages.offset).
218 218 to_a
219 219
220 220 render :layout => false if request.xhr?
221 221 end
222 222
223 223 def diff
224 224 @diff = @page.diff(params[:version], params[:version_from])
225 225 render_404 unless @diff
226 226 end
227 227
228 228 def annotate
229 229 @annotate = @page.annotate(params[:version])
230 230 render_404 unless @annotate
231 231 end
232 232
233 233 # Removes a wiki page and its history
234 234 # Children can be either set as root pages, removed or reassigned to another parent page
235 235 def destroy
236 236 return render_403 unless editable?
237 237
238 238 @descendants_count = @page.descendants.size
239 239 if @descendants_count > 0
240 240 case params[:todo]
241 241 when 'nullify'
242 242 # Nothing to do
243 243 when 'destroy'
244 244 # Removes all its descendants
245 245 @page.descendants.each(&:destroy)
246 246 when 'reassign'
247 247 # Reassign children to another parent page
248 248 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
249 249 return unless reassign_to
250 250 @page.children.each do |child|
251 251 child.update_attribute(:parent, reassign_to)
252 252 end
253 253 else
254 254 @reassignable_to = @wiki.pages - @page.self_and_descendants
255 255 # display the destroy form if it's a user request
256 256 return unless api_request?
257 257 end
258 258 end
259 259 @page.destroy
260 260 respond_to do |format|
261 261 format.html { redirect_to project_wiki_index_path(@project) }
262 262 format.api { render_api_ok }
263 263 end
264 264 end
265 265
266 266 def destroy_version
267 267 return render_403 unless editable?
268 268
269 269 @content = @page.content_for_version(params[:version])
270 270 @content.destroy
271 271 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
272 272 end
273 273
274 274 # Export wiki to a single pdf or html file
275 275 def export
276 276 @pages = @wiki.pages.
277 277 order('title').
278 278 includes([:content, {:attachments => :author}]).
279 279 to_a
280 280 respond_to do |format|
281 281 format.html {
282 282 export = render_to_string :action => 'export_multiple', :layout => false
283 283 send_data(export, :type => 'text/html', :filename => "wiki.html")
284 284 }
285 285 format.pdf {
286 286 send_data(wiki_pages_to_pdf(@pages, @project),
287 287 :type => 'application/pdf',
288 288 :filename => "#{@project.identifier}.pdf")
289 289 }
290 290 end
291 291 end
292 292
293 293 def preview
294 294 page = @wiki.find_page(params[:id])
295 295 # page is nil when previewing a new page
296 296 return render_403 unless page.nil? || editable?(page)
297 297 if page
298 298 @attachments += page.attachments
299 299 @previewed = page.content
300 300 end
301 301 @text = params[:content][:text]
302 302 render :partial => 'common/preview'
303 303 end
304 304
305 305 def add_attachment
306 306 return render_403 unless editable?
307 307 attachments = Attachment.attach_files(@page, params[:attachments])
308 308 render_attachment_warning_if_needed(@page)
309 309 redirect_to :action => 'show', :id => @page.title, :project_id => @project
310 310 end
311 311
312 312 private
313 313
314 314 def find_wiki
315 315 @project = Project.find(params[:project_id])
316 316 @wiki = @project.wiki
317 317 render_404 unless @wiki
318 318 rescue ActiveRecord::RecordNotFound
319 319 render_404
320 320 end
321 321
322 322 # Finds the requested page or a new page if it doesn't exist
323 323 def find_existing_or_new_page
324 324 @page = @wiki.find_or_new_page(params[:id])
325 325 if @wiki.page_found_with_redirect?
326 326 redirect_to_page @page
327 327 end
328 328 end
329 329
330 330 # Finds the requested page and returns a 404 error if it doesn't exist
331 331 def find_existing_page
332 332 @page = @wiki.find_page(params[:id])
333 333 if @page.nil?
334 334 render_404
335 335 return
336 336 end
337 337 if @wiki.page_found_with_redirect?
338 338 redirect_to_page @page
339 339 end
340 340 end
341 341
342 342 def redirect_to_page(page)
343 343 if page.project && page.project.visible?
344 344 redirect_to :action => action_name, :project_id => page.project, :id => page.title
345 345 else
346 346 render_404
347 347 end
348 348 end
349 349
350 350 # Returns true if the current user is allowed to edit the page, otherwise false
351 351 def editable?(page = @page)
352 352 page.editable_by?(User.current)
353 353 end
354 354
355 355 # Returns the default content of a new wiki page
356 356 def initial_page_content(page)
357 357 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
358 358 extend helper unless self.instance_of?(helper)
359 359 helper.instance_method(:initial_page_content).bind(self).call(page)
360 360 end
361 361
362 362 def load_pages_for_index
363 363 @pages = @wiki.pages.with_updated_on.
364 364 reorder("#{WikiPage.table_name}.title").
365 365 includes(:wiki => :project).
366 366 includes(:parent).
367 367 to_a
368 368 end
369 369 end
@@ -1,36 +1,36
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22
23 23 def index
24 24 @roles = Role.sorted.select(&:consider_workflow?)
25 25 @trackers = Tracker.sorted
26 26 @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count
27 27 end
28 28
29 29 def edit
30 30 find_trackers_roles_and_statuses_for_edit
31 31
32 32 if request.post? && @roles && @trackers && params[:transitions]
33 33 transitions = params[:transitions].deep_dup
34 34 transitions.each do |old_status_id, transitions_by_new_status|
35 35 transitions_by_new_status.each do |new_status_id, transition_by_rule|
36 36 transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
37 37 end
38 38 end
39 39 WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
40 40 flash[:notice] = l(:notice_successful_update)
41 41 redirect_to_referer_or workflows_edit_path
42 42 return
43 43 end
44 44
45 45 if @trackers && @roles && @statuses.any?
46 46 workflows = WorkflowTransition.where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id))
47 47 @workflows = {}
48 48 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
49 49 @workflows['author'] = workflows.select {|w| w.author}
50 50 @workflows['assignee'] = workflows.select {|w| w.assignee}
51 51 end
52 52 end
53 53
54 54 def permissions
55 55 find_trackers_roles_and_statuses_for_edit
56 56
57 57 if request.post? && @roles && @trackers && params[:permissions]
58 58 permissions = params[:permissions].deep_dup
59 59 permissions.each { |field, rule_by_status_id|
60 60 rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
61 61 }
62 62 WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
63 63 flash[:notice] = l(:notice_successful_update)
64 64 redirect_to_referer_or workflows_permissions_path
65 65 return
66 66 end
67 67
68 68 if @roles && @trackers
69 69 @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
70 70 @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
71 71 @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
72 72 @statuses.each {|status| @permissions[status.id] ||= {}}
73 73 end
74 74 end
75 75
76 76 def copy
77 77 @roles = Role.sorted.select(&:consider_workflow?)
78 78 @trackers = Tracker.sorted
79 79
80 80 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
81 81 @source_tracker = nil
82 82 else
83 83 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
84 84 end
85 85 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
86 86 @source_role = nil
87 87 else
88 88 @source_role = Role.find_by_id(params[:source_role_id].to_i)
89 89 end
90 90 @target_trackers = params[:target_tracker_ids].blank? ?
91 91 nil : Tracker.where(:id => params[:target_tracker_ids]).to_a
92 92 @target_roles = params[:target_role_ids].blank? ?
93 93 nil : Role.where(:id => params[:target_role_ids]).to_a
94 94 if request.post?
95 95 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
96 96 flash.now[:error] = l(:error_workflow_copy_source)
97 97 elsif @target_trackers.blank? || @target_roles.blank?
98 98 flash.now[:error] = l(:error_workflow_copy_target)
99 99 else
100 100 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
101 101 flash[:notice] = l(:notice_successful_update)
102 102 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
103 103 end
104 104 end
105 105 end
106 106
107 107 private
108 108
109 109 def find_trackers_roles_and_statuses_for_edit
110 110 find_roles
111 111 find_trackers
112 112 find_statuses
113 113 end
114 114
115 115 def find_roles
116 116 ids = Array.wrap(params[:role_id])
117 117 if ids == ['all']
118 118 @roles = Role.sorted.to_a
119 119 elsif ids.present?
120 120 @roles = Role.where(:id => ids).to_a
121 121 end
122 122 @roles = nil if @roles.blank?
123 123 end
124 124
125 125 def find_trackers
126 126 ids = Array.wrap(params[:tracker_id])
127 127 if ids == ['all']
128 128 @trackers = Tracker.sorted.to_a
129 129 elsif ids.present?
130 130 @trackers = Tracker.where(:id => ids).to_a
131 131 end
132 132 @trackers = nil if @trackers.blank?
133 133 end
134 134
135 135 def find_statuses
136 136 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
137 137 if @trackers && @used_statuses_only
138 138 @statuses = @trackers.map(&:issue_statuses).flatten.uniq.sort.presence
139 139 end
140 140 @statuses ||= IssueStatus.sorted.to_a
141 141 end
142 142 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ActivitiesHelper
21 21 def sort_activity_events(events)
22 22 events_by_group = events.group_by(&:event_group)
23 23 sorted_events = []
24 24 events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event|
25 25 if group_events = events_by_group.delete(event.event_group)
26 26 group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i|
27 27 sorted_events << [e, i > 0]
28 28 end
29 29 end
30 30 end
31 31 sorted_events
32 32 end
33 33 end
@@ -1,35 +1,35
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AdminHelper
21 21 def project_status_options_for_select(selected)
22 22 options_for_select([[l(:label_all), ''],
23 23 [l(:project_status_active), '1'],
24 24 [l(:project_status_closed), '5'],
25 25 [l(:project_status_archived), '9']], selected.to_s)
26 26 end
27 27
28 28 def plugin_data_for_updates(plugins)
29 29 data = {"v" => Redmine::VERSION.to_s, "p" => {}}
30 30 plugins.each do |plugin|
31 31 data["p"].merge! plugin.id => {"v" => plugin.version, "n" => plugin.name, "a" => plugin.author}
32 32 end
33 33 data
34 34 end
35 35 end
@@ -1,1312 +1,1312
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
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),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 97 html_options = options.slice!(:only_path)
98 98 url = send(route_method, attachment, attachment.filename, options)
99 99 link_to text, url, html_options
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, repository, options={})
106 106 if repository.is_a?(Project)
107 107 repository = repository.repository
108 108 end
109 109 text = options.delete(:text) || format_revision(revision)
110 110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 111 link_to(
112 112 h(text),
113 113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 114 :title => l(:label_revision_id, format_revision(revision)),
115 115 :accesskey => options[:accesskey]
116 116 )
117 117 end
118 118
119 119 # Generates a link to a message
120 120 def link_to_message(message, options={}, html_options = nil)
121 121 link_to(
122 122 message.subject.truncate(60),
123 123 board_message_path(message.board_id, message.parent_id || message.id, {
124 124 :r => (message.parent_id && message.id),
125 125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
126 126 }.merge(options)),
127 127 html_options
128 128 )
129 129 end
130 130
131 131 # Generates a link to a project if active
132 132 # Examples:
133 133 #
134 134 # link_to_project(project) # => link to the specified project overview
135 135 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
136 136 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
137 137 #
138 138 def link_to_project(project, options={}, html_options = nil)
139 139 if project.archived?
140 140 h(project.name)
141 141 else
142 142 link_to project.name, project_path(project, options), html_options
143 143 end
144 144 end
145 145
146 146 # Generates a link to a project settings if active
147 147 def link_to_project_settings(project, options={}, html_options=nil)
148 148 if project.active?
149 149 link_to project.name, settings_project_path(project, options), html_options
150 150 elsif project.archived?
151 151 h(project.name)
152 152 else
153 153 link_to project.name, project_path(project, options), html_options
154 154 end
155 155 end
156 156
157 157 # Generates a link to a version
158 158 def link_to_version(version, options = {})
159 159 return '' unless version && version.is_a?(Version)
160 160 options = {:title => format_date(version.effective_date)}.merge(options)
161 161 link_to_if version.visible?, format_version_name(version), version_path(version), options
162 162 end
163 163
164 164 # Helper that formats object for html or text rendering
165 165 def format_object(object, html=true, &block)
166 166 if block_given?
167 167 object = yield object
168 168 end
169 169 case object.class.name
170 170 when 'Array'
171 171 object.map {|o| format_object(o, html)}.join(', ').html_safe
172 172 when 'Time'
173 173 format_time(object)
174 174 when 'Date'
175 175 format_date(object)
176 176 when 'Fixnum'
177 177 object.to_s
178 178 when 'Float'
179 179 sprintf "%.2f", object
180 180 when 'User'
181 181 html ? link_to_user(object) : object.to_s
182 182 when 'Project'
183 183 html ? link_to_project(object) : object.to_s
184 184 when 'Version'
185 185 html ? link_to_version(object) : object.to_s
186 186 when 'TrueClass'
187 187 l(:general_text_Yes)
188 188 when 'FalseClass'
189 189 l(:general_text_No)
190 190 when 'Issue'
191 191 object.visible? && html ? link_to_issue(object) : "##{object.id}"
192 192 when 'CustomValue', 'CustomFieldValue'
193 193 if object.custom_field
194 194 f = object.custom_field.format.formatted_custom_value(self, object, html)
195 195 if f.nil? || f.is_a?(String)
196 196 f
197 197 else
198 198 format_object(f, html, &block)
199 199 end
200 200 else
201 201 object.value.to_s
202 202 end
203 203 else
204 204 html ? h(object) : object.to_s
205 205 end
206 206 end
207 207
208 208 def wiki_page_path(page, options={})
209 209 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
210 210 end
211 211
212 212 def thumbnail_tag(attachment)
213 213 link_to image_tag(thumbnail_path(attachment)),
214 214 named_attachment_path(attachment, attachment.filename),
215 215 :title => attachment.filename
216 216 end
217 217
218 218 def toggle_link(name, id, options={})
219 219 onclick = "$('##{id}').toggle(); "
220 220 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
221 221 onclick << "return false;"
222 222 link_to(name, "#", :onclick => onclick)
223 223 end
224 224
225 225 def format_activity_title(text)
226 226 h(truncate_single_line_raw(text, 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(text.to_s.truncate(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.shared? || 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, &block)
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? ? capture(project, &block) : 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, selected=params[:tab])
312 312 if tabs.any?
313 313 unless tabs.detect {|tab| tab[:name] == selected}
314 314 selected = nil
315 315 end
316 316 selected ||= tabs.first[:name]
317 317 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
318 318 else
319 319 content_tag 'p', l(:label_no_data), :class => "nodata"
320 320 end
321 321 end
322 322
323 323 # Renders the project quick-jump box
324 324 def render_project_jump_box
325 325 return unless User.current.logged?
326 326 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
327 327 if projects.any?
328 328 options =
329 329 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
330 330 '<option value="" disabled="disabled">---</option>').html_safe
331 331
332 332 options << project_tree_options_for_select(projects, :selected => @project) do |p|
333 333 { :value => project_path(:id => p, :jump => current_menu_item) }
334 334 end
335 335
336 336 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
337 337 end
338 338 end
339 339
340 340 def project_tree_options_for_select(projects, options = {})
341 341 s = ''.html_safe
342 342 if options[:include_blank]
343 343 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
344 344 end
345 345 project_tree(projects) do |project, level|
346 346 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
347 347 tag_options = {:value => project.id}
348 348 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
349 349 tag_options[:selected] = 'selected'
350 350 else
351 351 tag_options[:selected] = nil
352 352 end
353 353 tag_options.merge!(yield(project)) if block_given?
354 354 s << content_tag('option', name_prefix + h(project), tag_options)
355 355 end
356 356 s.html_safe
357 357 end
358 358
359 359 # Yields the given block for each project with its level in the tree
360 360 #
361 361 # Wrapper for Project#project_tree
362 362 def project_tree(projects, &block)
363 363 Project.project_tree(projects, &block)
364 364 end
365 365
366 366 def principals_check_box_tags(name, principals)
367 367 s = ''
368 368 principals.each do |principal|
369 369 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
370 370 end
371 371 s.html_safe
372 372 end
373 373
374 374 # Returns a string for users/groups option tags
375 375 def principals_options_for_select(collection, selected=nil)
376 376 s = ''
377 377 if collection.include?(User.current)
378 378 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
379 379 end
380 380 groups = ''
381 381 collection.sort.each do |element|
382 382 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
383 383 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
384 384 end
385 385 unless groups.empty?
386 386 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
387 387 end
388 388 s.html_safe
389 389 end
390 390
391 391 def option_tag(name, text, value, selected=nil, options={})
392 392 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
393 393 end
394 394
395 395 def truncate_single_line_raw(string, length)
396 396 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
397 397 end
398 398
399 399 # Truncates at line break after 250 characters or options[:length]
400 400 def truncate_lines(string, options={})
401 401 length = options[:length] || 250
402 402 if string.to_s =~ /\A(.{#{length}}.*?)$/m
403 403 "#{$1}..."
404 404 else
405 405 string
406 406 end
407 407 end
408 408
409 409 def anchor(text)
410 410 text.to_s.gsub(' ', '_')
411 411 end
412 412
413 413 def html_hours(text)
414 414 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
415 415 end
416 416
417 417 def authoring(created, author, options={})
418 418 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
419 419 end
420 420
421 421 def time_tag(time)
422 422 text = distance_of_time_in_words(Time.now, time)
423 423 if @project
424 424 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
425 425 else
426 426 content_tag('abbr', text, :title => format_time(time))
427 427 end
428 428 end
429 429
430 430 def syntax_highlight_lines(name, content)
431 431 lines = []
432 432 syntax_highlight(name, content).each_line { |line| lines << line }
433 433 lines
434 434 end
435 435
436 436 def syntax_highlight(name, content)
437 437 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
438 438 end
439 439
440 440 def to_path_param(path)
441 441 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
442 442 str.blank? ? nil : str
443 443 end
444 444
445 445 def reorder_links(name, url, method = :post)
446 446 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
447 447 url.merge({"#{name}[move_to]" => 'highest'}),
448 448 :method => method, :title => l(:label_sort_highest)) +
449 449 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
450 450 url.merge({"#{name}[move_to]" => 'higher'}),
451 451 :method => method, :title => l(:label_sort_higher)) +
452 452 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
453 453 url.merge({"#{name}[move_to]" => 'lower'}),
454 454 :method => method, :title => l(:label_sort_lower)) +
455 455 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
456 456 url.merge({"#{name}[move_to]" => 'lowest'}),
457 457 :method => method, :title => l(:label_sort_lowest))
458 458 end
459 459
460 460 def breadcrumb(*args)
461 461 elements = args.flatten
462 462 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
463 463 end
464 464
465 465 def other_formats_links(&block)
466 466 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
467 467 yield Redmine::Views::OtherFormatsBuilder.new(self)
468 468 concat('</p>'.html_safe)
469 469 end
470 470
471 471 def page_header_title
472 472 if @project.nil? || @project.new_record?
473 473 h(Setting.app_title)
474 474 else
475 475 b = []
476 476 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
477 477 if ancestors.any?
478 478 root = ancestors.shift
479 479 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
480 480 if ancestors.size > 2
481 481 b << "\xe2\x80\xa6"
482 482 ancestors = ancestors[-2, 2]
483 483 end
484 484 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
485 485 end
486 486 b << h(@project)
487 487 b.join(" \xc2\xbb ").html_safe
488 488 end
489 489 end
490 490
491 491 # Returns a h2 tag and sets the html title with the given arguments
492 492 def title(*args)
493 493 strings = args.map do |arg|
494 494 if arg.is_a?(Array) && arg.size >= 2
495 495 link_to(*arg)
496 496 else
497 497 h(arg.to_s)
498 498 end
499 499 end
500 500 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
501 501 content_tag('h2', strings.join(' &#187; ').html_safe)
502 502 end
503 503
504 504 # Sets the html title
505 505 # Returns the html title when called without arguments
506 506 # Current project name and app_title and automatically appended
507 507 # Exemples:
508 508 # html_title 'Foo', 'Bar'
509 509 # html_title # => 'Foo - Bar - My Project - Redmine'
510 510 def html_title(*args)
511 511 if args.empty?
512 512 title = @html_title || []
513 513 title << @project.name if @project
514 514 title << Setting.app_title unless Setting.app_title == title.last
515 515 title.reject(&:blank?).join(' - ')
516 516 else
517 517 @html_title ||= []
518 518 @html_title += args
519 519 end
520 520 end
521 521
522 522 # Returns the theme, controller name, and action as css classes for the
523 523 # HTML body.
524 524 def body_css_classes
525 525 css = []
526 526 if theme = Redmine::Themes.theme(Setting.ui_theme)
527 527 css << 'theme-' + theme.name
528 528 end
529 529
530 530 css << 'project-' + @project.identifier if @project && @project.identifier.present?
531 531 css << 'controller-' + controller_name
532 532 css << 'action-' + action_name
533 533 css.join(' ')
534 534 end
535 535
536 536 def accesskey(s)
537 537 @used_accesskeys ||= []
538 538 key = Redmine::AccessKeys.key_for(s)
539 539 return nil if @used_accesskeys.include?(key)
540 540 @used_accesskeys << key
541 541 key
542 542 end
543 543
544 544 # Formats text according to system settings.
545 545 # 2 ways to call this method:
546 546 # * with a String: textilizable(text, options)
547 547 # * with an object and one of its attribute: textilizable(issue, :description, options)
548 548 def textilizable(*args)
549 549 options = args.last.is_a?(Hash) ? args.pop : {}
550 550 case args.size
551 551 when 1
552 552 obj = options[:object]
553 553 text = args.shift
554 554 when 2
555 555 obj = args.shift
556 556 attr = args.shift
557 557 text = obj.send(attr).to_s
558 558 else
559 559 raise ArgumentError, 'invalid arguments to textilizable'
560 560 end
561 561 return '' if text.blank?
562 562 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
563 563 @only_path = only_path = options.delete(:only_path) == false ? false : true
564 564
565 565 text = text.dup
566 566 macros = catch_macros(text)
567 567 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
568 568
569 569 @parsed_headings = []
570 570 @heading_anchors = {}
571 571 @current_section = 0 if options[:edit_section_links]
572 572
573 573 parse_sections(text, project, obj, attr, only_path, options)
574 574 text = parse_non_pre_blocks(text, obj, macros) do |text|
575 575 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
576 576 send method_name, text, project, obj, attr, only_path, options
577 577 end
578 578 end
579 579 parse_headings(text, project, obj, attr, only_path, options)
580 580
581 581 if @parsed_headings.any?
582 582 replace_toc(text, @parsed_headings)
583 583 end
584 584
585 585 text.html_safe
586 586 end
587 587
588 588 def parse_non_pre_blocks(text, obj, macros)
589 589 s = StringScanner.new(text)
590 590 tags = []
591 591 parsed = ''
592 592 while !s.eos?
593 593 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
594 594 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
595 595 if tags.empty?
596 596 yield text
597 597 inject_macros(text, obj, macros) if macros.any?
598 598 else
599 599 inject_macros(text, obj, macros, false) if macros.any?
600 600 end
601 601 parsed << text
602 602 if tag
603 603 if closing
604 604 if tags.last == tag.downcase
605 605 tags.pop
606 606 end
607 607 else
608 608 tags << tag.downcase
609 609 end
610 610 parsed << full_tag
611 611 end
612 612 end
613 613 # Close any non closing tags
614 614 while tag = tags.pop
615 615 parsed << "</#{tag}>"
616 616 end
617 617 parsed
618 618 end
619 619
620 620 def parse_inline_attachments(text, project, obj, attr, only_path, options)
621 621 # when using an image link, try to use an attachment, if possible
622 622 attachments = options[:attachments] || []
623 623 attachments += obj.attachments if obj.respond_to?(:attachments)
624 624 if attachments.present?
625 625 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
626 626 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
627 627 # search for the picture in attachments
628 628 if found = Attachment.latest_attach(attachments, filename)
629 629 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
630 630 desc = found.description.to_s.gsub('"', '')
631 631 if !desc.blank? && alttext.blank?
632 632 alt = " title=\"#{desc}\" alt=\"#{desc}\""
633 633 end
634 634 "src=\"#{image_url}\"#{alt}"
635 635 else
636 636 m
637 637 end
638 638 end
639 639 end
640 640 end
641 641
642 642 # Wiki links
643 643 #
644 644 # Examples:
645 645 # [[mypage]]
646 646 # [[mypage|mytext]]
647 647 # wiki links can refer other project wikis, using project name or identifier:
648 648 # [[project:]] -> wiki starting page
649 649 # [[project:|mytext]]
650 650 # [[project:mypage]]
651 651 # [[project:mypage|mytext]]
652 652 def parse_wiki_links(text, project, obj, attr, only_path, options)
653 653 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
654 654 link_project = project
655 655 esc, all, page, title = $1, $2, $3, $5
656 656 if esc.nil?
657 657 if page =~ /^([^\:]+)\:(.*)$/
658 658 identifier, page = $1, $2
659 659 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
660 660 title ||= identifier if page.blank?
661 661 end
662 662
663 663 if link_project && link_project.wiki
664 664 # extract anchor
665 665 anchor = nil
666 666 if page =~ /^(.+?)\#(.+)$/
667 667 page, anchor = $1, $2
668 668 end
669 669 anchor = sanitize_anchor_name(anchor) if anchor.present?
670 670 # check if page exists
671 671 wiki_page = link_project.wiki.find_page(page)
672 672 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
673 673 "##{anchor}"
674 674 else
675 675 case options[:wiki_links]
676 676 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
677 677 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
678 678 else
679 679 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
680 680 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
681 681 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
682 682 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
683 683 end
684 684 end
685 685 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
686 686 else
687 687 # project or wiki doesn't exist
688 688 all
689 689 end
690 690 else
691 691 all
692 692 end
693 693 end
694 694 end
695 695
696 696 # Redmine links
697 697 #
698 698 # Examples:
699 699 # Issues:
700 700 # #52 -> Link to issue #52
701 701 # Changesets:
702 702 # r52 -> Link to revision 52
703 703 # commit:a85130f -> Link to scmid starting with a85130f
704 704 # Documents:
705 705 # document#17 -> Link to document with id 17
706 706 # document:Greetings -> Link to the document with title "Greetings"
707 707 # document:"Some document" -> Link to the document with title "Some document"
708 708 # Versions:
709 709 # version#3 -> Link to version with id 3
710 710 # version:1.0.0 -> Link to version named "1.0.0"
711 711 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
712 712 # Attachments:
713 713 # attachment:file.zip -> Link to the attachment of the current object named file.zip
714 714 # Source files:
715 715 # source:some/file -> Link to the file located at /some/file in the project's repository
716 716 # source:some/file@52 -> Link to the file's revision 52
717 717 # source:some/file#L120 -> Link to line 120 of the file
718 718 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
719 719 # export:some/file -> Force the download of the file
720 720 # Forum messages:
721 721 # message#1218 -> Link to message with id 1218
722 722 # Projects:
723 723 # project:someproject -> Link to project named "someproject"
724 724 # project#3 -> Link to project with id 3
725 725 #
726 726 # Links can refer other objects from other projects, using project identifier:
727 727 # identifier:r52
728 728 # identifier:document:"Some document"
729 729 # identifier:version:1.0.0
730 730 # identifier:source:some/file
731 731 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
732 732 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
733 733 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
734 734 if tag_content
735 735 $&
736 736 else
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 &&
753 753 (changeset = Changeset.visible.
754 754 find_by_repository_id_and_revision(repository.id, identifier))
755 755 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
756 756 {:only_path => only_path, :controller => 'repositories',
757 757 :action => 'revision', :id => project,
758 758 :repository_id => repository.identifier_param,
759 759 :rev => changeset.revision},
760 760 :class => 'changeset',
761 761 :title => truncate_single_line_raw(changeset.comments, 100))
762 762 end
763 763 end
764 764 elsif sep == '#'
765 765 oid = identifier.to_i
766 766 case prefix
767 767 when nil
768 768 if oid.to_s == identifier &&
769 769 issue = Issue.visible.find_by_id(oid)
770 770 anchor = comment_id ? "note-#{comment_id}" : nil
771 771 link = link_to("##{oid}#{comment_suffix}",
772 772 issue_path(issue, :only_path => only_path, :anchor => anchor),
773 773 :class => issue.css_classes,
774 774 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
775 775 end
776 776 when 'document'
777 777 if document = Document.visible.find_by_id(oid)
778 778 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
779 779 end
780 780 when 'version'
781 781 if version = Version.visible.find_by_id(oid)
782 782 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
783 783 end
784 784 when 'message'
785 785 if message = Message.visible.find_by_id(oid)
786 786 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
787 787 end
788 788 when 'forum'
789 789 if board = Board.visible.find_by_id(oid)
790 790 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
791 791 end
792 792 when 'news'
793 793 if news = News.visible.find_by_id(oid)
794 794 link = link_to(news.title, news_path(news, :only_path => only_path), :class => 'news')
795 795 end
796 796 when 'project'
797 797 if p = Project.visible.find_by_id(oid)
798 798 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
799 799 end
800 800 end
801 801 elsif sep == ':'
802 802 # removes the double quotes if any
803 803 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
804 804 name = CGI.unescapeHTML(name)
805 805 case prefix
806 806 when 'document'
807 807 if project && document = project.documents.visible.find_by_title(name)
808 808 link = link_to(document.title, document_path(document, :only_path => only_path), :class => 'document')
809 809 end
810 810 when 'version'
811 811 if project && version = project.versions.visible.find_by_name(name)
812 812 link = link_to(version.name, version_path(version, :only_path => only_path), :class => 'version')
813 813 end
814 814 when 'forum'
815 815 if project && board = project.boards.visible.find_by_name(name)
816 816 link = link_to(board.name, project_board_path(board.project, board, :only_path => only_path), :class => 'board')
817 817 end
818 818 when 'news'
819 819 if project && news = project.news.visible.find_by_title(name)
820 820 link = link_to(news.title, news_path(news, :only_path => only_path), :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_raw(changeset.comments, 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}"), {:only_path => only_path, :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 end
867 867
868 868 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
869 869
870 870 def parse_sections(text, project, obj, attr, only_path, options)
871 871 return unless options[:edit_section_links]
872 872 text.gsub!(HEADING_RE) do
873 873 heading = $1
874 874 @current_section += 1
875 875 if @current_section > 1
876 876 content_tag('div',
877 877 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
878 878 :class => 'contextual',
879 879 :title => l(:button_edit_section),
880 880 :id => "section-#{@current_section}") + heading.html_safe
881 881 else
882 882 heading
883 883 end
884 884 end
885 885 end
886 886
887 887 # Headings and TOC
888 888 # Adds ids and links to headings unless options[:headings] is set to false
889 889 def parse_headings(text, project, obj, attr, only_path, options)
890 890 return if options[:headings] == false
891 891
892 892 text.gsub!(HEADING_RE) do
893 893 level, attrs, content = $2.to_i, $3, $4
894 894 item = strip_tags(content).strip
895 895 anchor = sanitize_anchor_name(item)
896 896 # used for single-file wiki export
897 897 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
898 898 @heading_anchors[anchor] ||= 0
899 899 idx = (@heading_anchors[anchor] += 1)
900 900 if idx > 1
901 901 anchor = "#{anchor}-#{idx}"
902 902 end
903 903 @parsed_headings << [level, anchor, item]
904 904 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
905 905 end
906 906 end
907 907
908 908 MACROS_RE = /(
909 909 (!)? # escaping
910 910 (
911 911 \{\{ # opening tag
912 912 ([\w]+) # macro name
913 913 (\(([^\n\r]*?)\))? # optional arguments
914 914 ([\n\r].*?[\n\r])? # optional block of text
915 915 \}\} # closing tag
916 916 )
917 917 )/mx unless const_defined?(:MACROS_RE)
918 918
919 919 MACRO_SUB_RE = /(
920 920 \{\{
921 921 macro\((\d+)\)
922 922 \}\}
923 923 )/x unless const_defined?(:MACRO_SUB_RE)
924 924
925 925 # Extracts macros from text
926 926 def catch_macros(text)
927 927 macros = {}
928 928 text.gsub!(MACROS_RE) do
929 929 all, macro = $1, $4.downcase
930 930 if macro_exists?(macro) || all =~ MACRO_SUB_RE
931 931 index = macros.size
932 932 macros[index] = all
933 933 "{{macro(#{index})}}"
934 934 else
935 935 all
936 936 end
937 937 end
938 938 macros
939 939 end
940 940
941 941 # Executes and replaces macros in text
942 942 def inject_macros(text, obj, macros, execute=true)
943 943 text.gsub!(MACRO_SUB_RE) do
944 944 all, index = $1, $2.to_i
945 945 orig = macros.delete(index)
946 946 if execute && orig && orig =~ MACROS_RE
947 947 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
948 948 if esc.nil?
949 949 h(exec_macro(macro, obj, args, block) || all)
950 950 else
951 951 h(all)
952 952 end
953 953 elsif orig
954 954 h(orig)
955 955 else
956 956 h(all)
957 957 end
958 958 end
959 959 end
960 960
961 961 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
962 962
963 963 # Renders the TOC with given headings
964 964 def replace_toc(text, headings)
965 965 text.gsub!(TOC_RE) do
966 966 left_align, right_align = $2, $3
967 967 # Keep only the 4 first levels
968 968 headings = headings.select{|level, anchor, item| level <= 4}
969 969 if headings.empty?
970 970 ''
971 971 else
972 972 div_class = 'toc'
973 973 div_class << ' right' if right_align
974 974 div_class << ' left' if left_align
975 975 out = "<ul class=\"#{div_class}\"><li>"
976 976 root = headings.map(&:first).min
977 977 current = root
978 978 started = false
979 979 headings.each do |level, anchor, item|
980 980 if level > current
981 981 out << '<ul><li>' * (level - current)
982 982 elsif level < current
983 983 out << "</li></ul>\n" * (current - level) + "</li><li>"
984 984 elsif started
985 985 out << '</li><li>'
986 986 end
987 987 out << "<a href=\"##{anchor}\">#{item}</a>"
988 988 current = level
989 989 started = true
990 990 end
991 991 out << '</li></ul>' * (current - root)
992 992 out << '</li></ul>'
993 993 end
994 994 end
995 995 end
996 996
997 997 # Same as Rails' simple_format helper without using paragraphs
998 998 def simple_format_without_paragraph(text)
999 999 text.to_s.
1000 1000 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1001 1001 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1002 1002 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1003 1003 html_safe
1004 1004 end
1005 1005
1006 1006 def lang_options_for_select(blank=true)
1007 1007 (blank ? [["(auto)", ""]] : []) + languages_options
1008 1008 end
1009 1009
1010 1010 def labelled_form_for(*args, &proc)
1011 1011 args << {} unless args.last.is_a?(Hash)
1012 1012 options = args.last
1013 1013 if args.first.is_a?(Symbol)
1014 1014 options.merge!(:as => args.shift)
1015 1015 end
1016 1016 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1017 1017 form_for(*args, &proc)
1018 1018 end
1019 1019
1020 1020 def labelled_fields_for(*args, &proc)
1021 1021 args << {} unless args.last.is_a?(Hash)
1022 1022 options = args.last
1023 1023 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1024 1024 fields_for(*args, &proc)
1025 1025 end
1026 1026
1027 1027 def error_messages_for(*objects)
1028 1028 html = ""
1029 1029 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1030 1030 errors = objects.map {|o| o.errors.full_messages}.flatten
1031 1031 if errors.any?
1032 1032 html << "<div id='errorExplanation'><ul>\n"
1033 1033 errors.each do |error|
1034 1034 html << "<li>#{h error}</li>\n"
1035 1035 end
1036 1036 html << "</ul></div>\n"
1037 1037 end
1038 1038 html.html_safe
1039 1039 end
1040 1040
1041 1041 def delete_link(url, options={})
1042 1042 options = {
1043 1043 :method => :delete,
1044 1044 :data => {:confirm => l(:text_are_you_sure)},
1045 1045 :class => 'icon icon-del'
1046 1046 }.merge(options)
1047 1047
1048 1048 link_to l(:button_delete), url, options
1049 1049 end
1050 1050
1051 1051 def preview_link(url, form, target='preview', options={})
1052 1052 content_tag 'a', l(:label_preview), {
1053 1053 :href => "#",
1054 1054 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1055 1055 :accesskey => accesskey(:preview)
1056 1056 }.merge(options)
1057 1057 end
1058 1058
1059 1059 def link_to_function(name, function, html_options={})
1060 1060 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1061 1061 end
1062 1062
1063 1063 # Helper to render JSON in views
1064 1064 def raw_json(arg)
1065 1065 arg.to_json.to_s.gsub('/', '\/').html_safe
1066 1066 end
1067 1067
1068 1068 def back_url
1069 1069 url = params[:back_url]
1070 1070 if url.nil? && referer = request.env['HTTP_REFERER']
1071 1071 url = CGI.unescape(referer.to_s)
1072 1072 end
1073 1073 url
1074 1074 end
1075 1075
1076 1076 def back_url_hidden_field_tag
1077 1077 url = back_url
1078 1078 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1079 1079 end
1080 1080
1081 1081 def check_all_links(form_name)
1082 1082 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1083 1083 " | ".html_safe +
1084 1084 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1085 1085 end
1086 1086
1087 1087 def toggle_checkboxes_link(selector)
1088 1088 link_to_function image_tag('toggle_check.png'),
1089 1089 "toggleCheckboxesBySelector('#{selector}')",
1090 1090 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1091 1091 end
1092 1092
1093 1093 def progress_bar(pcts, options={})
1094 1094 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1095 1095 pcts = pcts.collect(&:round)
1096 1096 pcts[1] = pcts[1] - pcts[0]
1097 1097 pcts << (100 - pcts[1] - pcts[0])
1098 1098 width = options[:width] || '100px;'
1099 1099 legend = options[:legend] || ''
1100 1100 content_tag('table',
1101 1101 content_tag('tr',
1102 1102 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1103 1103 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1104 1104 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1105 1105 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1106 1106 content_tag('p', legend, :class => 'percent').html_safe
1107 1107 end
1108 1108
1109 1109 def checked_image(checked=true)
1110 1110 if checked
1111 1111 image_tag 'toggle_check.png'
1112 1112 end
1113 1113 end
1114 1114
1115 1115 def context_menu(url)
1116 1116 unless @context_menu_included
1117 1117 content_for :header_tags do
1118 1118 javascript_include_tag('context_menu') +
1119 1119 stylesheet_link_tag('context_menu')
1120 1120 end
1121 1121 if l(:direction) == 'rtl'
1122 1122 content_for :header_tags do
1123 1123 stylesheet_link_tag('context_menu_rtl')
1124 1124 end
1125 1125 end
1126 1126 @context_menu_included = true
1127 1127 end
1128 1128 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1129 1129 end
1130 1130
1131 1131 def calendar_for(field_id)
1132 1132 include_calendar_headers_tags
1133 1133 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1134 1134 end
1135 1135
1136 1136 def include_calendar_headers_tags
1137 1137 unless @calendar_headers_tags_included
1138 1138 tags = ''.html_safe
1139 1139 @calendar_headers_tags_included = true
1140 1140 content_for :header_tags do
1141 1141 start_of_week = Setting.start_of_week
1142 1142 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1143 1143 # Redmine uses 1..7 (monday..sunday) in settings and locales
1144 1144 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1145 1145 start_of_week = start_of_week.to_i % 7
1146 1146 tags << javascript_tag(
1147 1147 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1148 1148 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1149 1149 path_to_image('/images/calendar.png') +
1150 1150 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1151 1151 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1152 1152 "beforeShow: beforeShowDatePicker};")
1153 1153 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1154 1154 unless jquery_locale == 'en'
1155 1155 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1156 1156 end
1157 1157 tags
1158 1158 end
1159 1159 end
1160 1160 end
1161 1161
1162 1162 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1163 1163 # Examples:
1164 1164 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1165 1165 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1166 1166 #
1167 1167 def stylesheet_link_tag(*sources)
1168 1168 options = sources.last.is_a?(Hash) ? sources.pop : {}
1169 1169 plugin = options.delete(:plugin)
1170 1170 sources = sources.map do |source|
1171 1171 if plugin
1172 1172 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1173 1173 elsif current_theme && current_theme.stylesheets.include?(source)
1174 1174 current_theme.stylesheet_path(source)
1175 1175 else
1176 1176 source
1177 1177 end
1178 1178 end
1179 1179 super *sources, options
1180 1180 end
1181 1181
1182 1182 # Overrides Rails' image_tag with themes and plugins support.
1183 1183 # Examples:
1184 1184 # image_tag('image.png') # => picks image.png from the current theme or defaults
1185 1185 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1186 1186 #
1187 1187 def image_tag(source, options={})
1188 1188 if plugin = options.delete(:plugin)
1189 1189 source = "/plugin_assets/#{plugin}/images/#{source}"
1190 1190 elsif current_theme && current_theme.images.include?(source)
1191 1191 source = current_theme.image_path(source)
1192 1192 end
1193 1193 super source, options
1194 1194 end
1195 1195
1196 1196 # Overrides Rails' javascript_include_tag with plugins support
1197 1197 # Examples:
1198 1198 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1199 1199 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1200 1200 #
1201 1201 def javascript_include_tag(*sources)
1202 1202 options = sources.last.is_a?(Hash) ? sources.pop : {}
1203 1203 if plugin = options.delete(:plugin)
1204 1204 sources = sources.map do |source|
1205 1205 if plugin
1206 1206 "/plugin_assets/#{plugin}/javascripts/#{source}"
1207 1207 else
1208 1208 source
1209 1209 end
1210 1210 end
1211 1211 end
1212 1212 super *sources, options
1213 1213 end
1214 1214
1215 1215 def sidebar_content?
1216 1216 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1217 1217 end
1218 1218
1219 1219 def view_layouts_base_sidebar_hook_response
1220 1220 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1221 1221 end
1222 1222
1223 1223 def email_delivery_enabled?
1224 1224 !!ActionMailer::Base.perform_deliveries
1225 1225 end
1226 1226
1227 1227 # Returns the avatar image tag for the given +user+ if avatars are enabled
1228 1228 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1229 1229 def avatar(user, options = { })
1230 1230 if Setting.gravatar_enabled?
1231 1231 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1232 1232 email = nil
1233 1233 if user.respond_to?(:mail)
1234 1234 email = user.mail
1235 1235 elsif user.to_s =~ %r{<(.+?)>}
1236 1236 email = $1
1237 1237 end
1238 1238 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1239 1239 else
1240 1240 ''
1241 1241 end
1242 1242 end
1243 1243
1244 1244 def sanitize_anchor_name(anchor)
1245 1245 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1246 1246 end
1247 1247
1248 1248 # Returns the javascript tags that are included in the html layout head
1249 1249 def javascript_heads
1250 1250 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1251 1251 unless User.current.pref.warn_on_leaving_unsaved == '0'
1252 1252 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1253 1253 end
1254 1254 tags
1255 1255 end
1256 1256
1257 1257 def favicon
1258 1258 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1259 1259 end
1260 1260
1261 1261 # Returns the path to the favicon
1262 1262 def favicon_path
1263 1263 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1264 1264 image_path(icon)
1265 1265 end
1266 1266
1267 1267 # Returns the full URL to the favicon
1268 1268 def favicon_url
1269 1269 # TODO: use #image_url introduced in Rails4
1270 1270 path = favicon_path
1271 1271 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1272 1272 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1273 1273 end
1274 1274
1275 1275 def robot_exclusion_tag
1276 1276 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1277 1277 end
1278 1278
1279 1279 # Returns true if arg is expected in the API response
1280 1280 def include_in_api_response?(arg)
1281 1281 unless @included_in_api_response
1282 1282 param = params[:include]
1283 1283 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1284 1284 @included_in_api_response.collect!(&:strip)
1285 1285 end
1286 1286 @included_in_api_response.include?(arg.to_s)
1287 1287 end
1288 1288
1289 1289 # Returns options or nil if nometa param or X-Redmine-Nometa header
1290 1290 # was set in the request
1291 1291 def api_meta(options)
1292 1292 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1293 1293 # compatibility mode for activeresource clients that raise
1294 1294 # an error when deserializing an array with attributes
1295 1295 nil
1296 1296 else
1297 1297 options
1298 1298 end
1299 1299 end
1300 1300
1301 1301 private
1302 1302
1303 1303 def wiki_helper
1304 1304 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1305 1305 extend helper
1306 1306 return self
1307 1307 end
1308 1308
1309 1309 def link_to_content_update(text, url_params = {}, html_options = {})
1310 1310 link_to(text, url_params, html_options)
1311 1311 end
1312 1312 end
@@ -1,64 +1,64
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AttachmentsHelper
21 21
22 22 def container_attachments_edit_path(container)
23 23 object_attachments_edit_path container.class.name.underscore.pluralize, container.id
24 24 end
25 25
26 26 def container_attachments_path(container)
27 27 object_attachments_path container.class.name.underscore.pluralize, container.id
28 28 end
29 29
30 30 # Displays view/delete links to the attachments of the given object
31 31 # Options:
32 32 # :author -- author names are not displayed if set to false
33 33 # :thumbails -- display thumbnails if enabled in settings
34 34 def link_to_attachments(container, options = {})
35 35 options.assert_valid_keys(:author, :thumbnails)
36 36
37 37 if container.attachments.any?
38 38 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
39 39 render :partial => 'attachments/links',
40 40 :locals => {
41 41 :container => container,
42 42 :attachments => container.attachments,
43 43 :options => options,
44 44 :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)
45 45 }
46 46 end
47 47 end
48 48
49 49 def render_api_attachment(attachment, api)
50 50 api.attachment do
51 51 api.id attachment.id
52 52 api.filename attachment.filename
53 53 api.filesize attachment.filesize
54 54 api.content_type attachment.content_type
55 55 api.description attachment.description
56 56 api.content_url download_named_attachment_url(attachment, attachment.filename)
57 57 if attachment.thumbnailable?
58 58 api.thumbnail_url thumbnail_url(attachment)
59 59 end
60 60 api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
61 61 api.created_on attachment.created_on
62 62 end
63 63 end
64 64 end
@@ -1,24 +1,24
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module CalendarsHelper
21 21 def link_to_previous_month(year, month, options={})
22 22 target_year, target_month = if month == 1
23 23 [year - 1, 12]
24 24 else
25 25 [year, month - 1]
26 26 end
27 27
28 28 name = if target_month == 12
29 29 "#{month_name(target_month)} #{target_year}"
30 30 else
31 31 "#{month_name(target_month)}"
32 32 end
33 33
34 34 # \xc2\xab(utf-8) = &#171;
35 35 link_to_month(("\xc2\xab " + name), target_year, target_month, options)
36 36 end
37 37
38 38 def link_to_next_month(year, month, options={})
39 39 target_year, target_month = if month == 12
40 40 [year + 1, 1]
41 41 else
42 42 [year, month + 1]
43 43 end
44 44
45 45 name = if target_month == 1
46 46 "#{month_name(target_month)} #{target_year}"
47 47 else
48 48 "#{month_name(target_month)}"
49 49 end
50 50
51 51 # \xc2\xbb(utf-8) = &#187;
52 52 link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
53 53 end
54 54
55 55 def link_to_month(link_name, year, month, options={})
56 56 link_to_content_update(h(link_name), params.merge(:year => year, :month => month), options)
57 57 end
58 58 end
@@ -1,50 +1,50
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ContextMenusHelper
21 21 def context_menu_link(name, url, options={})
22 22 options[:class] ||= ''
23 23 if options.delete(:selected)
24 24 options[:class] << ' icon-checked disabled'
25 25 options[:disabled] = true
26 26 end
27 27 if options.delete(:disabled)
28 28 options.delete(:method)
29 29 options.delete(:data)
30 30 options[:onclick] = 'return false;'
31 31 options[:class] << ' disabled'
32 32 url = '#'
33 33 end
34 34 link_to h(name), url, options
35 35 end
36 36
37 37 def bulk_update_custom_field_context_menu_link(field, text, value)
38 38 context_menu_link h(text),
39 39 bulk_update_issues_path(:ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back),
40 40 :method => :post,
41 41 :selected => (@issue && @issue.custom_field_value(field) == value)
42 42 end
43 43
44 44 def bulk_update_time_entry_custom_field_context_menu_link(field, text, value)
45 45 context_menu_link h(text),
46 46 bulk_update_time_entries_path(:ids => @time_entries.map(&:id).sort, :time_entry => {'custom_field_values' => {field.id => value}}, :back_url => @back),
47 47 :method => :post,
48 48 :selected => (@time_entry && @time_entry.custom_field_value(field) == value)
49 49 end
50 50 end
@@ -1,159 +1,159
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 render_custom_fields_tabs(types)
44 44 tabs = CUSTOM_FIELDS_TABS.select {|h| types.include?(h[:name]) }
45 45 render_tabs tabs
46 46 end
47 47
48 48 def custom_field_type_options
49 49 CUSTOM_FIELDS_TABS.map {|h| [l(h[:label]), h[:name]]}
50 50 end
51 51
52 52 def render_custom_field_format_partial(form, custom_field)
53 53 partial = custom_field.format.form_partial
54 54 if partial
55 55 render :partial => custom_field.format.form_partial, :locals => {:f => form, :custom_field => custom_field}
56 56 end
57 57 end
58 58
59 59 def custom_field_tag_name(prefix, custom_field)
60 60 name = "#{prefix}[custom_field_values][#{custom_field.id}]"
61 61 name << "[]" if custom_field.multiple?
62 62 name
63 63 end
64 64
65 65 def custom_field_tag_id(prefix, custom_field)
66 66 "#{prefix}_custom_field_values_#{custom_field.id}"
67 67 end
68 68
69 69 # Return custom field html tag corresponding to its format
70 70 def custom_field_tag(prefix, custom_value)
71 71 custom_value.custom_field.format.edit_tag self,
72 72 custom_field_tag_id(prefix, custom_value.custom_field),
73 73 custom_field_tag_name(prefix, custom_value.custom_field),
74 74 custom_value,
75 75 :class => "#{custom_value.custom_field.field_format}_cf"
76 76 end
77 77
78 78 # Return custom field label tag
79 79 def custom_field_label_tag(name, custom_value, options={})
80 80 required = options[:required] || custom_value.custom_field.is_required?
81 81 title = custom_value.custom_field.description.presence
82 82 content = content_tag 'span', custom_value.custom_field.name, :title => title
83 83
84 84 content_tag "label", content +
85 85 (required ? " <span class=\"required\">*</span>".html_safe : ""),
86 86 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
87 87 end
88 88
89 89 # Return custom field tag with its label tag
90 90 def custom_field_tag_with_label(name, custom_value, options={})
91 91 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
92 92 end
93 93
94 94 # Returns the custom field tag for when bulk editing objects
95 95 def custom_field_tag_for_bulk_edit(prefix, custom_field, objects=nil, value='')
96 96 custom_field.format.bulk_edit_tag self,
97 97 custom_field_tag_id(prefix, custom_field),
98 98 custom_field_tag_name(prefix, custom_field),
99 99 custom_field,
100 100 objects,
101 101 value,
102 102 :class => "#{custom_field.field_format}_cf"
103 103 end
104 104
105 105 # Return a string used to display a custom value
106 106 def show_value(custom_value, html=true)
107 107 format_object(custom_value, html)
108 108 end
109 109
110 110 # Return a string used to display a custom value
111 111 def format_value(value, custom_field)
112 112 format_object(custom_field.format.formatted_value(self, custom_field, value, false), false)
113 113 end
114 114
115 115 # Return an array of custom field formats which can be used in select_tag
116 116 def custom_field_formats_for_select(custom_field)
117 117 Redmine::FieldFormat.as_select(custom_field.class.customized_class.name)
118 118 end
119 119
120 120 # Yields the given block for each custom field value of object that should be
121 121 # displayed, with the custom field and the formatted value as arguments
122 122 def render_custom_field_values(object, &block)
123 123 object.visible_custom_field_values.each do |custom_value|
124 124 formatted = show_value(custom_value)
125 125 if formatted.present?
126 126 yield custom_value.custom_field, formatted
127 127 end
128 128 end
129 129 end
130 130
131 131 # Renders the custom_values in api views
132 132 def render_api_custom_values(custom_values, api)
133 133 api.array :custom_fields do
134 134 custom_values.each do |custom_value|
135 135 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
136 136 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
137 137 api.custom_field attrs do
138 138 if custom_value.value.is_a?(Array)
139 139 api.array :value do
140 140 custom_value.value.each do |value|
141 141 api.value value unless value.blank?
142 142 end
143 143 end
144 144 else
145 145 api.value custom_value.value
146 146 end
147 147 end
148 148 end
149 149 end unless custom_values.empty?
150 150 end
151 151
152 152 def edit_tag_style_tag(form, options={})
153 153 select_options = [[l(:label_drop_down_list), ''], [l(:label_checkboxes), 'check_box']]
154 154 if options[:include_radio]
155 155 select_options << [l(:label_radio_buttons), 'radio']
156 156 end
157 157 form.select :edit_tag_style, select_options, :label => :label_display
158 158 end
159 159 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module GanttHelper
21 21
22 22 def gantt_zoom_link(gantt, in_or_out)
23 23 case in_or_out
24 24 when :in
25 25 if gantt.zoom < 4
26 26 link_to_content_update l(:text_zoom_in),
27 27 params.merge(gantt.params.merge(:zoom => (gantt.zoom + 1))),
28 28 :class => 'icon icon-zoom-in'
29 29 else
30 30 content_tag(:span, l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe
31 31 end
32 32
33 33 when :out
34 34 if gantt.zoom > 1
35 35 link_to_content_update l(:text_zoom_out),
36 36 params.merge(gantt.params.merge(:zoom => (gantt.zoom - 1))),
37 37 :class => 'icon icon-zoom-out'
38 38 else
39 39 content_tag(:span, l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
40 40 end
41 41 end
42 42 end
43 43 end
@@ -1,46 +1,46
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module GroupsHelper
21 21 def group_settings_tabs(group)
22 22 tabs = []
23 23 tabs << {:name => 'general', :partial => 'groups/general', :label => :label_general}
24 24 tabs << {:name => 'users', :partial => 'groups/users', :label => :label_user_plural} if group.givable?
25 25 tabs << {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
26 26 tabs
27 27 end
28 28
29 29 def render_principals_for_new_group_users(group, limit=100)
30 30 scope = User.active.sorted.not_in_group(group).like(params[:q])
31 31 principal_count = scope.count
32 32 principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
33 33 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
34 34
35 35 s = content_tag('div',
36 36 content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals'),
37 37 :class => 'objects-selection'
38 38 )
39 39
40 40 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
41 41 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
42 42 }
43 43
44 44 s + content_tag('p', links, :class => 'pagination')
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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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.to_a
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, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
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 # Returns the number of descendants for an array of issues
184 184 def issues_descendant_count(issues)
185 185 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
186 186 ids -= issues.map(&:id)
187 187 ids.size
188 188 end
189 189
190 190 def issues_destroy_confirmation_message(issues)
191 191 issues = [issues] unless issues.is_a?(Array)
192 192 message = l(:text_issues_destroy_confirmation)
193 193
194 194 descendant_count = issues_descendant_count(issues)
195 195 if descendant_count > 0
196 196 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
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 to_a
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')
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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MailHandlerHelper
21 21 end
@@ -1,38 +1,38
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MembersHelper
21 21 def render_principals_for_new_members(project, limit=100)
22 22 scope = Principal.active.visible.sorted.not_member_of(project).like(params[:q])
23 23 principal_count = scope.count
24 24 principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
25 25 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
26 26
27 27 s = content_tag('div',
28 28 content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals'),
29 29 :class => 'objects-selection'
30 30 )
31 31
32 32 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
33 33 link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
34 34 }
35 35
36 36 s + content_tag('p', links, :class => 'pagination')
37 37 end
38 38 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MessagesHelper
21 21 end
@@ -1,75 +1,75
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MyHelper
21 21 def calendar_items(startdt, enddt)
22 22 Issue.visible.
23 23 where(:project_id => User.current.projects.map(&:id)).
24 24 where("(start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)", startdt, enddt, startdt, enddt).
25 25 includes(:project, :tracker, :priority, :assigned_to).
26 26 references(:project, :tracker, :priority, :assigned_to).
27 27 to_a
28 28 end
29 29
30 30 def documents_items
31 31 Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).to_a
32 32 end
33 33
34 34 def issuesassignedtome_items
35 35 Issue.visible.open.
36 36 where(:assigned_to_id => ([User.current.id] + User.current.group_ids)).
37 37 limit(10).
38 38 includes(:status, :project, :tracker, :priority).
39 39 references(:status, :project, :tracker, :priority).
40 40 order("#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC").
41 41 to_a
42 42 end
43 43
44 44 def issuesreportedbyme_items
45 45 Issue.visible.
46 46 where(:author_id => User.current.id).
47 47 limit(10).
48 48 includes(:status, :project, :tracker).
49 49 references(:status, :project, :tracker).
50 50 order("#{Issue.table_name}.updated_on DESC").
51 51 to_a
52 52 end
53 53
54 54 def issueswatched_items
55 55 Issue.visible.on_active_project.watched_by(User.current.id).recently_updated.limit(10).to_a
56 56 end
57 57
58 58 def news_items
59 59 News.visible.
60 60 where(:project_id => User.current.projects.map(&:id)).
61 61 limit(10).
62 62 includes(:project, :author).
63 63 references(:project, :author).
64 64 order("#{News.table_name}.created_on DESC").
65 65 to_a
66 66 end
67 67
68 68 def timelog_items
69 69 TimeEntry.
70 70 where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, Date.today - 6, Date.today).
71 71 joins(:activity, :project, {:issue => [:tracker, :status]}).
72 72 order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC").
73 73 to_a
74 74 end
75 75 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module NewsHelper
21 21 end
@@ -1,56 +1,56
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module PrincipalMembershipsHelper
21 21 def render_principal_memberships(principal)
22 22 render :partial => 'principal_memberships/index', :locals => {:principal => principal}
23 23 end
24 24
25 25 def call_table_header_hook(principal)
26 26 if principal.is_a?(Group)
27 27 call_hook :view_groups_memberships_table_header, :group => principal
28 28 else
29 29 call_hook :view_users_memberships_table_header, :user => principal
30 30 end
31 31 end
32 32
33 33 def call_table_row_hook(principal, membership)
34 34 if principal.is_a?(Group)
35 35 call_hook :view_groups_memberships_table_row, :group => principal, :membership => membership
36 36 else
37 37 call_hook :view_users_memberships_table_row, :user => principal, :membership => membership
38 38 end
39 39 end
40 40
41 41 def new_principal_membership_path(principal, *args)
42 42 if principal.is_a?(Group)
43 43 new_group_membership_path(principal, *args)
44 44 else
45 45 new_user_membership_path(principal, *args)
46 46 end
47 47 end
48 48
49 49 def principal_membership_path(principal, membership, *args)
50 50 if principal.is_a?(Group)
51 51 group_membership_path(principal, membership, *args)
52 52 else
53 53 user_membership_path(principal, membership, *args)
54 54 end
55 55 end
56 56 end
@@ -1,115 +1,115
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ProjectsHelper
21 21 def project_settings_tabs
22 22 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
23 23 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
24 24 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
25 25 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
26 26 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
27 27 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
28 28 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
29 29 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
30 30 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
31 31 ]
32 32 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
33 33 end
34 34
35 35 def parent_project_select_tag(project)
36 36 selected = project.parent
37 37 # retrieve the requested parent project
38 38 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
39 39 if parent_id
40 40 selected = (parent_id.blank? ? nil : Project.find(parent_id))
41 41 end
42 42
43 43 options = ''
44 44 options << "<option value=''>&nbsp;</option>" if project.allowed_parents.include?(nil)
45 45 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
46 46 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
47 47 end
48 48
49 49 def render_project_action_links
50 50 links = []
51 51 if User.current.allowed_to?(:add_project, nil, :global => true)
52 52 links << link_to(l(:label_project_new), new_project_path, :class => 'icon icon-add')
53 53 end
54 54 if User.current.allowed_to?(:view_issues, nil, :global => true)
55 55 links << link_to(l(:label_issue_view_all), issues_path)
56 56 end
57 57 if User.current.allowed_to?(:view_time_entries, nil, :global => true)
58 58 links << link_to(l(:label_overall_spent_time), time_entries_path)
59 59 end
60 60 links << link_to(l(:label_overall_activity), activity_path)
61 61 links.join(" | ").html_safe
62 62 end
63 63
64 64 # Renders the projects index
65 65 def render_project_hierarchy(projects)
66 66 render_project_nested_lists(projects) do |project|
67 67 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
68 68 if project.description.present?
69 69 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
70 70 end
71 71 s
72 72 end
73 73 end
74 74
75 75 # Returns a set of options for a select field, grouped by project.
76 76 def version_options_for_select(versions, selected=nil)
77 77 grouped = Hash.new {|h,k| h[k] = []}
78 78 versions.each do |version|
79 79 grouped[version.project.name] << [version.name, version.id]
80 80 end
81 81
82 82 selected = selected.is_a?(Version) ? selected.id : selected
83 83 if grouped.keys.size > 1
84 84 grouped_options_for_select(grouped, selected)
85 85 else
86 86 options_for_select((grouped.values.first || []), selected)
87 87 end
88 88 end
89 89
90 90 def format_version_sharing(sharing)
91 91 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
92 92 l("label_version_sharing_#{sharing}")
93 93 end
94 94
95 95 def render_api_includes(project, api)
96 96 api.array :trackers do
97 97 project.trackers.each do |tracker|
98 98 api.tracker(:id => tracker.id, :name => tracker.name)
99 99 end
100 100 end if include_in_api_response?('trackers')
101 101
102 102 api.array :issue_categories do
103 103 project.issue_categories.each do |category|
104 104 api.issue_category(:id => category.id, :name => category.name)
105 105 end
106 106 end if include_in_api_response?('issue_categories')
107 107
108 108 api.array :enabled_modules do
109 109 project.enabled_modules.each do |enabled_module|
110 110 api.enabled_module(:id => enabled_module.id, :name => enabled_module.name)
111 111 end
112 112 end if include_in_api_response?('enabled_modules')
113 113
114 114 end
115 115 end
@@ -1,225 +1,225
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 ungrouped = []
25 25 grouped = {}
26 26 query.available_filters.map do |field, field_options|
27 27 if field_options[:type] == :relation
28 28 group = :label_related_issues
29 29 elsif field =~ /^(.+)\./
30 30 # association filters
31 31 group = "field_#{$1}"
32 32 elsif %w(member_of_group assigned_to_role).include?(field)
33 33 group = :field_assigned_to
34 34 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 35 group = :label_date
36 36 end
37 37 if group
38 38 (grouped[group] ||= []) << [field_options[:name], field]
39 39 else
40 40 ungrouped << [field_options[:name], field]
41 41 end
42 42 end
43 43 # Don't group dates if there's only one (eg. time entries filters)
44 44 if grouped[:label_date].try(:size) == 1
45 45 ungrouped << grouped.delete(:label_date).first
46 46 end
47 47 s = options_for_select([[]] + ungrouped)
48 48 if grouped.present?
49 49 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 50 s << grouped_options_for_select(localized_grouped)
51 51 end
52 52 s
53 53 end
54 54
55 55 def query_filters_hidden_tags(query)
56 56 tags = ''.html_safe
57 57 query.filters.each do |field, options|
58 58 tags << hidden_field_tag("f[]", field, :id => nil)
59 59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 60 options[:values].each do |value|
61 61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 62 end
63 63 end
64 64 tags
65 65 end
66 66
67 67 def query_columns_hidden_tags(query)
68 68 tags = ''.html_safe
69 69 query.columns.each do |column|
70 70 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 71 end
72 72 tags
73 73 end
74 74
75 75 def query_hidden_tags(query)
76 76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 77 end
78 78
79 79 def available_block_columns_tags(query)
80 80 tags = ''.html_safe
81 81 query.available_block_columns.each do |column|
82 82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 83 end
84 84 tags
85 85 end
86 86
87 87 def query_available_inline_columns_options(query)
88 88 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
89 89 end
90 90
91 91 def query_selected_inline_columns_options(query)
92 92 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
93 93 end
94 94
95 95 def render_query_columns_selection(query, options={})
96 96 tag_name = (options[:name] || 'c') + '[]'
97 97 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
98 98 end
99 99
100 100 def column_header(column)
101 101 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
102 102 :default_order => column.default_order) :
103 103 content_tag('th', h(column.caption))
104 104 end
105 105
106 106 def column_content(column, issue)
107 107 value = column.value_object(issue)
108 108 if value.is_a?(Array)
109 109 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
110 110 else
111 111 column_value(column, issue, value)
112 112 end
113 113 end
114 114
115 115 def column_value(column, issue, value)
116 116 case column.name
117 117 when :id
118 118 link_to value, issue_path(issue)
119 119 when :subject
120 120 link_to value, issue_path(issue)
121 121 when :parent
122 122 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
123 123 when :description
124 124 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
125 125 when :done_ratio
126 126 progress_bar(value, :width => '80px')
127 127 when :relations
128 128 content_tag('span',
129 129 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
130 130 :class => value.css_classes_for(issue))
131 131 else
132 132 format_object(value)
133 133 end
134 134 end
135 135
136 136 def csv_content(column, issue)
137 137 value = column.value_object(issue)
138 138 if value.is_a?(Array)
139 139 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
140 140 else
141 141 csv_value(column, issue, value)
142 142 end
143 143 end
144 144
145 145 def csv_value(column, object, value)
146 146 format_object(value, false) do |value|
147 147 case value.class.name
148 148 when 'Float'
149 149 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
150 150 when 'IssueRelation'
151 151 value.to_s(object)
152 152 when 'Issue'
153 153 if object.is_a?(TimeEntry)
154 154 "#{value.tracker} ##{value.id}: #{value.subject}"
155 155 else
156 156 value.id
157 157 end
158 158 else
159 159 value
160 160 end
161 161 end
162 162 end
163 163
164 164 def query_to_csv(items, query, options={})
165 165 encoding = l(:general_csv_encoding)
166 166 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
167 167 query.available_block_columns.each do |column|
168 168 if options[column.name].present?
169 169 columns << column
170 170 end
171 171 end
172 172
173 173 export = CSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
174 174 # csv header fields
175 175 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
176 176 # csv lines
177 177 items.each do |item|
178 178 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
179 179 end
180 180 end
181 181 export
182 182 end
183 183
184 184 # Retrieve query from session or build a new query
185 185 def retrieve_query
186 186 if !params[:query_id].blank?
187 187 cond = "project_id IS NULL"
188 188 cond << " OR project_id = #{@project.id}" if @project
189 189 @query = IssueQuery.where(cond).find(params[:query_id])
190 190 raise ::Unauthorized unless @query.visible?
191 191 @query.project = @project
192 192 session[:query] = {:id => @query.id, :project_id => @query.project_id}
193 193 sort_clear
194 194 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
195 195 # Give it a name, required to be valid
196 196 @query = IssueQuery.new(:name => "_")
197 197 @query.project = @project
198 198 @query.build_from_params(params)
199 199 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
200 200 else
201 201 # retrieve from session
202 202 @query = nil
203 203 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
204 204 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
205 205 @query.project = @project
206 206 end
207 207 end
208 208
209 209 def retrieve_query_from_session
210 210 if session[:query]
211 211 if session[:query][:id]
212 212 @query = IssueQuery.find_by_id(session[:query][:id])
213 213 return unless @query
214 214 else
215 215 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
216 216 end
217 217 if session[:query].has_key?(:project_id)
218 218 @query.project_id = session[:query][:project_id]
219 219 else
220 220 @query.project = @project
221 221 end
222 222 @query
223 223 end
224 224 end
225 225 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ReportsHelper
21 21
22 22 def aggregate(data, criteria)
23 23 a = 0
24 24 data.each { |row|
25 25 match = 1
26 26 criteria.each { |k, v|
27 27 match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && (v == 0 ? ['f', false] : ['t', true]).include?(row[k]))
28 28 } unless criteria.nil?
29 29 a = a + row["total"].to_i if match == 1
30 30 } unless data.nil?
31 31 a
32 32 end
33 33
34 34 def aggregate_link(data, criteria, *args)
35 35 a = aggregate data, criteria
36 36 a > 0 ? link_to(h(a), *args) : '-'
37 37 end
38 38
39 39 def aggregate_path(project, field, row, options={})
40 40 parameters = {:set_filter => 1, :subproject_id => '!*', field => row.id}.merge(options)
41 41 project_issues_path(row.is_a?(Project) ? row : project, parameters)
42 42 end
43 43 end
@@ -1,310 +1,310
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RepositoriesHelper
21 21 def format_revision(revision)
22 22 if revision.respond_to? :format_identifier
23 23 revision.format_identifier
24 24 else
25 25 revision.to_s
26 26 end
27 27 end
28 28
29 29 def truncate_at_line_break(text, length = 255)
30 30 if text
31 31 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
32 32 end
33 33 end
34 34
35 35 def render_properties(properties)
36 36 unless properties.nil? || properties.empty?
37 37 content = ''
38 38 properties.keys.sort.each do |property|
39 39 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
40 40 end
41 41 content_tag('ul', content.html_safe, :class => 'properties')
42 42 end
43 43 end
44 44
45 45 def render_changeset_changes
46 46 changes = @changeset.filechanges.limit(1000).reorder('path').collect do |change|
47 47 case change.action
48 48 when 'A'
49 49 # Detects moved/copied files
50 50 if !change.from_path.blank?
51 51 change.action =
52 52 @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
53 53 end
54 54 change
55 55 when 'D'
56 56 @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
57 57 else
58 58 change
59 59 end
60 60 end.compact
61 61
62 62 tree = { }
63 63 changes.each do |change|
64 64 p = tree
65 65 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
66 66 path = ''
67 67 dirs.each do |dir|
68 68 path += '/' + dir
69 69 p[:s] ||= {}
70 70 p = p[:s]
71 71 p[path] ||= {}
72 72 p = p[path]
73 73 end
74 74 p[:c] = change
75 75 end
76 76 render_changes_tree(tree[:s])
77 77 end
78 78
79 79 def render_changes_tree(tree)
80 80 return '' if tree.nil?
81 81 output = ''
82 82 output << '<ul>'
83 83 tree.keys.sort.each do |file|
84 84 style = 'change'
85 85 text = File.basename(h(file))
86 86 if s = tree[file][:s]
87 87 style << ' folder'
88 88 path_param = to_path_param(@repository.relative_path(file))
89 89 text = link_to(h(text), :controller => 'repositories',
90 90 :action => 'show',
91 91 :id => @project,
92 92 :repository_id => @repository.identifier_param,
93 93 :path => path_param,
94 94 :rev => @changeset.identifier)
95 95 output << "<li class='#{style}'>#{text}"
96 96 output << render_changes_tree(s)
97 97 output << "</li>"
98 98 elsif c = tree[file][:c]
99 99 style << " change-#{c.action}"
100 100 path_param = to_path_param(@repository.relative_path(c.path))
101 101 text = link_to(h(text), :controller => 'repositories',
102 102 :action => 'entry',
103 103 :id => @project,
104 104 :repository_id => @repository.identifier_param,
105 105 :path => path_param,
106 106 :rev => @changeset.identifier) unless c.action == 'D'
107 107 text << " - #{h(c.revision)}" unless c.revision.blank?
108 108 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
109 109 :action => 'diff',
110 110 :id => @project,
111 111 :repository_id => @repository.identifier_param,
112 112 :path => path_param,
113 113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 115 output << "<li class='#{style}'>#{text}</li>"
116 116 end
117 117 end
118 118 output << '</ul>'
119 119 output.html_safe
120 120 end
121 121
122 122 def repository_field_tags(form, repository)
123 123 method = repository.class.name.demodulize.underscore + "_field_tags"
124 124 if repository.is_a?(Repository) &&
125 125 respond_to?(method) && method != 'repository_field_tags'
126 126 send(method, form, repository)
127 127 end
128 128 end
129 129
130 130 def scm_select_tag(repository)
131 131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 132 Redmine::Scm::Base.all.each do |scm|
133 133 if Setting.enabled_scm.include?(scm) ||
134 134 (repository && repository.class.name.demodulize == scm)
135 135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 136 end
137 137 end
138 138 select_tag('repository_scm',
139 139 options_for_select(scm_options, repository.class.name.demodulize),
140 140 :disabled => (repository && !repository.new_record?),
141 141 :data => {:remote => true, :method => 'get'})
142 142 end
143 143
144 144 def with_leading_slash(path)
145 145 path.to_s.starts_with?('/') ? path : "/#{path}"
146 146 end
147 147
148 148 def subversion_field_tags(form, repository)
149 149 content_tag('p', form.text_field(:url, :size => 60, :required => true,
150 150 :disabled => !repository.safe_attribute?('url')) +
151 151 scm_path_info_tag(repository)) +
152 152 content_tag('p', form.text_field(:login, :size => 30)) +
153 153 content_tag('p', form.password_field(
154 154 :password, :size => 30, :name => 'ignore',
155 155 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
156 156 :onfocus => "this.value=''; this.name='repository[password]';",
157 157 :onchange => "this.name='repository[password]';"))
158 158 end
159 159
160 160 def darcs_field_tags(form, repository)
161 161 content_tag('p', form.text_field(
162 162 :url, :label => l(:field_path_to_repository),
163 163 :size => 60, :required => true,
164 164 :disabled => !repository.safe_attribute?('url')) +
165 165 scm_path_info_tag(repository)) +
166 166 scm_log_encoding_tag(form, repository)
167 167 end
168 168
169 169 def mercurial_field_tags(form, repository)
170 170 content_tag('p', form.text_field(
171 171 :url, :label => l(:field_path_to_repository),
172 172 :size => 60, :required => true,
173 173 :disabled => !repository.safe_attribute?('url')
174 174 ) +
175 175 scm_path_info_tag(repository)) +
176 176 scm_path_encoding_tag(form, repository)
177 177 end
178 178
179 179 def git_field_tags(form, repository)
180 180 content_tag('p', form.text_field(
181 181 :url, :label => l(:field_path_to_repository),
182 182 :size => 60, :required => true,
183 183 :disabled => !repository.safe_attribute?('url')
184 184 ) +
185 185 scm_path_info_tag(repository)) +
186 186 scm_path_encoding_tag(form, repository) +
187 187 content_tag('p', form.check_box(
188 188 :extra_report_last_commit,
189 189 :label => l(:label_git_report_last_commit)
190 190 ))
191 191 end
192 192
193 193 def cvs_field_tags(form, repository)
194 194 content_tag('p', form.text_field(
195 195 :root_url,
196 196 :label => l(:field_cvsroot),
197 197 :size => 60, :required => true,
198 198 :disabled => !repository.safe_attribute?('root_url')) +
199 199 scm_path_info_tag(repository)) +
200 200 content_tag('p', form.text_field(
201 201 :url,
202 202 :label => l(:field_cvs_module),
203 203 :size => 30, :required => true,
204 204 :disabled => !repository.safe_attribute?('url'))) +
205 205 scm_log_encoding_tag(form, repository) +
206 206 scm_path_encoding_tag(form, repository)
207 207 end
208 208
209 209 def bazaar_field_tags(form, repository)
210 210 content_tag('p', form.text_field(
211 211 :url, :label => l(:field_path_to_repository),
212 212 :size => 60, :required => true,
213 213 :disabled => !repository.safe_attribute?('url')) +
214 214 scm_path_info_tag(repository)) +
215 215 scm_log_encoding_tag(form, repository)
216 216 end
217 217
218 218 def filesystem_field_tags(form, repository)
219 219 content_tag('p', form.text_field(
220 220 :url, :label => l(:field_root_directory),
221 221 :size => 60, :required => true,
222 222 :disabled => !repository.safe_attribute?('url')) +
223 223 scm_path_info_tag(repository)) +
224 224 scm_path_encoding_tag(form, repository)
225 225 end
226 226
227 227 def scm_path_info_tag(repository)
228 228 text = scm_path_info(repository)
229 229 if text.present?
230 230 content_tag('em', text, :class => 'info')
231 231 else
232 232 ''
233 233 end
234 234 end
235 235
236 236 def scm_path_info(repository)
237 237 scm_name = repository.scm_name.to_s.downcase
238 238
239 239 info_from_config = Redmine::Configuration["scm_#{scm_name}_path_info"].presence
240 240 return info_from_config.html_safe if info_from_config
241 241
242 242 l("text_#{scm_name}_repository_note", :default => '')
243 243 end
244 244
245 245 def scm_log_encoding_tag(form, repository)
246 246 select = form.select(
247 247 :log_encoding,
248 248 [nil] + Setting::ENCODINGS,
249 249 :label => l(:field_commit_logs_encoding),
250 250 :required => true
251 251 )
252 252 content_tag('p', select)
253 253 end
254 254
255 255 def scm_path_encoding_tag(form, repository)
256 256 select = form.select(
257 257 :path_encoding,
258 258 [nil] + Setting::ENCODINGS,
259 259 :label => l(:field_scm_path_encoding)
260 260 )
261 261 content_tag('p', select + content_tag('em', l(:text_scm_path_encoding_note), :class => 'info'))
262 262 end
263 263
264 264 def index_commits(commits, heads)
265 265 return nil if commits.nil? or commits.first.parents.nil?
266 266 refs_map = {}
267 267 heads.each do |head|
268 268 refs_map[head.scmid] ||= []
269 269 refs_map[head.scmid] << head
270 270 end
271 271 commits_by_scmid = {}
272 272 commits.reverse.each_with_index do |commit, commit_index|
273 273 commits_by_scmid[commit.scmid] = {
274 274 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
275 275 :rdmid => commit_index,
276 276 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
277 277 :scmid => commit.scmid,
278 278 :href => block_given? ? yield(commit.scmid) : commit.scmid
279 279 }
280 280 end
281 281 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
282 282 space = nil
283 283 heads.each do |head|
284 284 if commits_by_scmid.include? head.scmid
285 285 space = index_head((space || -1) + 1, head, commits_by_scmid)
286 286 end
287 287 end
288 288 # when no head matched anything use first commit
289 289 space ||= index_head(0, commits.first, commits_by_scmid)
290 290 return commits_by_scmid, space
291 291 end
292 292
293 293 def index_head(space, commit, commits_by_scmid)
294 294 stack = [[space, commits_by_scmid[commit.scmid]]]
295 295 max_space = space
296 296 until stack.empty?
297 297 space, commit = stack.pop
298 298 commit[:space] = space if commit[:space].nil?
299 299 space -= 1
300 300 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
301 301 parent_commit = commits_by_scmid[parent_scmid]
302 302 if parent_commit and parent_commit[:space].nil?
303 303 stack.unshift [space += 1, parent_commit]
304 304 end
305 305 end
306 306 max_space = space if max_space < space
307 307 end
308 308 max_space
309 309 end
310 310 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RolesHelper
21 21 end
@@ -1,69 +1,69
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RoutesHelper
21 21
22 22 # Returns the path to project issues or to the cross-project
23 23 # issue list if project is nil
24 24 def _project_issues_path(project, *args)
25 25 if project
26 26 project_issues_path(project, *args)
27 27 else
28 28 issues_path(*args)
29 29 end
30 30 end
31 31
32 32 def _project_calendar_path(project, *args)
33 33 project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
34 34 end
35 35
36 36 def _project_gantt_path(project, *args)
37 37 project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
38 38 end
39 39
40 40 def _time_entries_path(project, issue, *args)
41 41 if issue
42 42 issue_time_entries_path(issue, *args)
43 43 elsif project
44 44 project_time_entries_path(project, *args)
45 45 else
46 46 time_entries_path(*args)
47 47 end
48 48 end
49 49
50 50 def _report_time_entries_path(project, issue, *args)
51 51 if issue
52 52 report_issue_time_entries_path(issue, *args)
53 53 elsif project
54 54 report_project_time_entries_path(project, *args)
55 55 else
56 56 report_time_entries_path(*args)
57 57 end
58 58 end
59 59
60 60 def _new_time_entry_path(project, issue, *args)
61 61 if issue
62 62 new_issue_time_entry_path(issue, *args)
63 63 elsif project
64 64 new_project_time_entry_path(project, *args)
65 65 else
66 66 new_time_entry_path(*args)
67 67 end
68 68 end
69 69 end
@@ -1,69 +1,69
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module SearchHelper
21 21 def highlight_tokens(text, tokens)
22 22 return text unless text && tokens && !tokens.empty?
23 23 re_tokens = tokens.collect {|t| Regexp.escape(t)}
24 24 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
25 25 result = ''
26 26 text.split(regexp).each_with_index do |words, i|
27 27 if result.length > 1200
28 28 # maximum length of the preview reached
29 29 result << '...'
30 30 break
31 31 end
32 32 if i.even?
33 33 result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
34 34 else
35 35 t = (tokens.index(words.downcase) || 0) % 4
36 36 result << content_tag('span', h(words), :class => "highlight token-#{t}")
37 37 end
38 38 end
39 39 result.html_safe
40 40 end
41 41
42 42 def type_label(t)
43 43 l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
44 44 end
45 45
46 46 def project_select_tag
47 47 options = [[l(:label_project_all), 'all']]
48 48 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
49 49 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
50 50 options << [@project.name, ''] unless @project.nil?
51 51 label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
52 52 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
53 53 end
54 54
55 55 def render_results_by_type(results_by_type)
56 56 links = []
57 57 # Sorts types by results count
58 58 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
59 59 c = results_by_type[t]
60 60 next if c == 0
61 61 text = "#{type_label(t)} (#{c})"
62 62 links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
63 63 :all_words => params[:all_words], :scope => params[:scope], t => 1)
64 64 end
65 65 ('<ul>'.html_safe +
66 66 links.map {|link| content_tag('li', link)}.join(' ').html_safe +
67 67 '</ul>'.html_safe) unless links.empty?
68 68 end
69 69 end
@@ -1,128 +1,128
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 tag_data = notifiable.parent.present? ?
88 88 {:parent_notifiable => notifiable.parent} :
89 89 {:disables => "input[data-parent-notifiable=#{notifiable.name}]"}
90 90
91 91 tag = check_box_tag('settings[notified_events][]',
92 92 notifiable.name,
93 93 Setting.notified_events.include?(notifiable.name),
94 94 :id => nil,
95 95 :data => tag_data)
96 96
97 97 text = l_or_humanize(notifiable.name, :prefix => 'label_')
98 98
99 99 options = {}
100 100 if notifiable.parent.present?
101 101 options[:class] = "parent"
102 102 end
103 103
104 104 content_tag(:label, tag + text, options)
105 105 end
106 106
107 107 def link_copied_issue_options
108 108 options = [
109 109 [:general_text_Yes, 'yes'],
110 110 [:general_text_No, 'no'],
111 111 [:label_ask, 'ask']
112 112 ]
113 113
114 114 options.map {|label, value| [l(label), value.to_s]}
115 115 end
116 116
117 117 def cross_project_subtasks_options
118 118 options = [
119 119 [:label_disabled, ''],
120 120 [:label_cross_project_system, 'system'],
121 121 [:label_cross_project_tree, 'tree'],
122 122 [:label_cross_project_hierarchy, 'hierarchy'],
123 123 [:label_cross_project_descendants, 'descendants']
124 124 ]
125 125
126 126 options.map {|label, value| [l(label), value.to_s]}
127 127 end
128 128 end
@@ -1,141 +1,141
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 format_criteria_value(criteria_options, value)
75 75 if value.blank?
76 76 "[#{l(:label_none)}]"
77 77 elsif k = criteria_options[:klass]
78 78 obj = k.find_by_id(value.to_i)
79 79 if obj.is_a?(Issue)
80 80 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
81 81 else
82 82 obj
83 83 end
84 84 elsif cf = criteria_options[:custom_field]
85 85 format_value(value, cf)
86 86 else
87 87 value.to_s
88 88 end
89 89 end
90 90
91 91 def report_to_csv(report)
92 92 decimal_separator = l(:general_csv_decimal_separator)
93 93 export = CSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
94 94 # Column headers
95 95 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
96 96 headers += report.periods
97 97 headers << l(:label_total_time)
98 98 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
99 99 c.to_s,
100 100 l(:general_csv_encoding) ) }
101 101 # Content
102 102 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
103 103 # Total row
104 104 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total_time), l(:general_csv_encoding))
105 105 row = [ str_total ] + [''] * (report.criteria.size - 1)
106 106 total = 0
107 107 report.periods.each do |period|
108 108 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
109 109 total += sum
110 110 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
111 111 end
112 112 row << ("%.2f" % total).gsub('.',decimal_separator)
113 113 csv << row
114 114 end
115 115 export
116 116 end
117 117
118 118 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
119 119 decimal_separator = l(:general_csv_decimal_separator)
120 120 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
121 121 hours_for_value = select_hours(hours, criteria[level], value)
122 122 next if hours_for_value.empty?
123 123 row = [''] * level
124 124 row << Redmine::CodesetUtil.from_utf8(
125 125 format_criteria_value(available_criteria[criteria[level]], value).to_s,
126 126 l(:general_csv_encoding) )
127 127 row += [''] * (criteria.length - level - 1)
128 128 total = 0
129 129 periods.each do |period|
130 130 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
131 131 total += sum
132 132 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
133 133 end
134 134 row << ("%.2f" % total).gsub('.',decimal_separator)
135 135 csv << row
136 136 if criteria.length > level + 1
137 137 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
138 138 end
139 139 end
140 140 end
141 141 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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.givable.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,76 +1,76
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module VersionsHelper
21 21
22 22 def version_anchor(version)
23 23 if @project == version.project
24 24 anchor version.name
25 25 else
26 26 anchor "#{version.project.try(:identifier)}-#{version.name}"
27 27 end
28 28 end
29 29
30 30 def version_filtered_issues_path(version, options = {})
31 31 options = {:fixed_version_id => version, :set_filter => 1}.merge(options)
32 32 project = case version.sharing
33 33 when 'hierarchy', 'tree'
34 34 if version.project && version.project.root.visible?
35 35 version.project.root
36 36 else
37 37 version.project
38 38 end
39 39 when 'system'
40 40 nil
41 41 else
42 42 version.project
43 43 end
44 44
45 45 if project
46 46 project_issues_path(project, options)
47 47 else
48 48 issues_path(options)
49 49 end
50 50 end
51 51
52 52 STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
53 53
54 54 def render_issue_status_by(version, criteria)
55 55 criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
56 56
57 57 h = Hash.new {|k,v| k[v] = [0, 0]}
58 58 begin
59 59 # Total issue count
60 60 Issue.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][0] = s}
61 61 # Open issues count
62 62 Issue.open.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][1] = s}
63 63 rescue ActiveRecord::RecordNotFound
64 64 # When grouping by an association, Rails throws this exception if there's no result (bug)
65 65 end
66 66 # Sort with nil keys in last position
67 67 counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
68 68 max = counts.collect {|c| c[:total]}.max
69 69
70 70 render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
71 71 end
72 72
73 73 def status_by_options_for_select(value)
74 74 options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
75 75 end
76 76 end
@@ -1,78 +1,78
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WatchersHelper
21 21
22 22 def watcher_link(objects, user)
23 23 return '' unless user && user.logged?
24 24 objects = Array.wrap(objects)
25 25 return '' unless objects.any?
26 26
27 27 watched = Watcher.any_watched?(objects, user)
28 28 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
29 29 text = watched ? l(:button_unwatch) : l(:button_watch)
30 30 url = watch_path(
31 31 :object_type => objects.first.class.to_s.underscore,
32 32 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
33 33 )
34 34 method = watched ? 'delete' : 'post'
35 35
36 36 link_to text, url, :remote => true, :method => method, :class => css
37 37 end
38 38
39 39 # Returns the css class used to identify watch links for a given +object+
40 40 def watcher_css(objects)
41 41 objects = Array.wrap(objects)
42 42 id = (objects.size == 1 ? objects.first.id : 'bulk')
43 43 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
44 44 end
45 45
46 46 # Returns a comma separated list of users watching the given object
47 47 def watchers_list(object)
48 48 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
49 49 content = ''.html_safe
50 50 lis = object.watcher_users.collect do |user|
51 51 s = ''.html_safe
52 52 s << avatar(user, :size => "16").to_s
53 53 s << link_to_user(user, :class => 'user')
54 54 if remove_allowed
55 55 url = {:controller => 'watchers',
56 56 :action => 'destroy',
57 57 :object_type => object.class.to_s.underscore,
58 58 :object_id => object.id,
59 59 :user_id => user}
60 60 s << ' '
61 61 s << link_to(image_tag('delete.png'), url,
62 62 :remote => true, :method => 'delete', :class => "delete")
63 63 end
64 64 content << content_tag('li', s, :class => "user-#{user.id}")
65 65 end
66 66 content.present? ? content_tag('ul', content, :class => 'watchers') : content
67 67 end
68 68
69 69 def watchers_checkboxes(object, users, checked=nil)
70 70 users.map do |user|
71 71 c = checked.nil? ? object.watched_by?(user) : checked
72 72 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
73 73 content_tag 'label', "#{tag} #{h(user)}".html_safe,
74 74 :id => "issue_watcher_user_ids_#{user.id}",
75 75 :class => "floating"
76 76 end.join.html_safe
77 77 end
78 78 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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,53 +1,53
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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_wiki_options_for_select(page)
39 39 projects = Project.allowed_to(:rename_wiki_pages).joins(:wiki).preload(:wiki).to_a
40 40 projects << page.project unless projects.include?(page.project)
41 41
42 42 project_tree_options_for_select(projects, :selected => page.project) do |project|
43 43 wiki_id = project.wiki.try(:id)
44 44 {:value => wiki_id, :selected => wiki_id == page.wiki_id}
45 45 end
46 46 end
47 47
48 48 def wiki_page_breadcrumb(page)
49 49 breadcrumb(page.ancestors.reverse.collect {|parent|
50 50 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
51 51 })
52 52 end
53 53 end
@@ -1,95 +1,95
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WorkflowsHelper
21 21 def options_for_workflow_select(name, objects, selected, options={})
22 22 option_tags = ''.html_safe
23 23 multiple = false
24 24 if selected
25 25 if selected.size == objects.size
26 26 selected = 'all'
27 27 else
28 28 selected = selected.map(&:id)
29 29 if selected.size > 1
30 30 multiple = true
31 31 end
32 32 end
33 33 else
34 34 selected = objects.first.try(:id)
35 35 end
36 36 all_tag_options = {:value => 'all', :selected => (selected == 'all')}
37 37 if multiple
38 38 all_tag_options.merge!(:style => "display:none;")
39 39 end
40 40 option_tags << content_tag('option', l(:label_all), all_tag_options)
41 41 option_tags << options_from_collection_for_select(objects, "id", "name", selected)
42 42 select_tag name, option_tags, {:multiple => multiple}.merge(options)
43 43 end
44 44
45 45 def field_required?(field)
46 46 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
47 47 end
48 48
49 49 def field_permission_tag(permissions, status, field, roles)
50 50 name = field.is_a?(CustomField) ? field.id.to_s : field
51 51 options = [["", ""], [l(:label_readonly), "readonly"]]
52 52 options << [l(:label_required), "required"] unless field_required?(field)
53 53 html_options = {}
54 54
55 55 if perm = permissions[status.id][name]
56 56 if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
57 57 options << [l(:label_no_change_option), "no_change"]
58 58 selected = 'no_change'
59 59 else
60 60 selected = perm.first
61 61 end
62 62 end
63 63
64 64 hidden = field.is_a?(CustomField) &&
65 65 !field.visible? &&
66 66 !roles.detect {|role| role.custom_fields.to_a.include?(field)}
67 67
68 68 if hidden
69 69 options[0][0] = l(:label_hidden)
70 70 selected = ''
71 71 html_options[:disabled] = true
72 72 end
73 73
74 74 select_tag("permissions[#{status.id}][#{name}]", options_for_select(options, selected), html_options)
75 75 end
76 76
77 77 def transition_tag(workflows, old_status, new_status, name)
78 78 w = workflows.select {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id}.size
79 79
80 80 tag_name = "transitions[#{ old_status.id }][#{new_status.id}][#{name}]"
81 81 if w == 0 || w == @roles.size * @trackers.size
82 82
83 83 hidden_field_tag(tag_name, "0", :id => nil) +
84 84 check_box_tag(tag_name, "1", w != 0,
85 85 :class => "old-status-#{old_status.id} new-status-#{new_status.id}")
86 86 else
87 87 select_tag tag_name,
88 88 options_for_select([
89 89 [l(:general_text_Yes), "1"],
90 90 [l(:general_text_No), "0"],
91 91 [l(:label_no_change_option), "no_change"]
92 92 ], "no_change")
93 93 end
94 94 end
95 95 end
@@ -1,376 +1,376
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19 require "fileutils"
20 20
21 21 class Attachment < ActiveRecord::Base
22 22 belongs_to :container, :polymorphic => true
23 23 belongs_to :author, :class_name => "User"
24 24
25 25 validates_presence_of :filename, :author
26 26 validates_length_of :filename, :maximum => 255
27 27 validates_length_of :disk_filename, :maximum => 255
28 28 validates_length_of :description, :maximum => 255
29 29 validate :validate_max_file_size
30 30 attr_protected :id
31 31
32 32 acts_as_event :title => :filename,
33 33 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
34 34
35 35 acts_as_activity_provider :type => 'files',
36 36 :permission => :view_files,
37 37 :author_key => :author_id,
38 38 :scope => select("#{Attachment.table_name}.*").
39 39 joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
40 40 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )")
41 41
42 42 acts_as_activity_provider :type => 'documents',
43 43 :permission => :view_documents,
44 44 :author_key => :author_id,
45 45 :scope => select("#{Attachment.table_name}.*").
46 46 joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
47 47 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
48 48
49 49 cattr_accessor :storage_path
50 50 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
51 51
52 52 cattr_accessor :thumbnails_storage_path
53 53 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
54 54
55 55 before_create :files_to_final_location
56 56 after_destroy :delete_from_disk
57 57
58 58 # Returns an unsaved copy of the attachment
59 59 def copy(attributes=nil)
60 60 copy = self.class.new
61 61 copy.attributes = self.attributes.dup.except("id", "downloads")
62 62 copy.attributes = attributes if attributes
63 63 copy
64 64 end
65 65
66 66 def validate_max_file_size
67 67 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
68 68 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
69 69 end
70 70 end
71 71
72 72 def file=(incoming_file)
73 73 unless incoming_file.nil?
74 74 @temp_file = incoming_file
75 75 if @temp_file.size > 0
76 76 if @temp_file.respond_to?(:original_filename)
77 77 self.filename = @temp_file.original_filename
78 78 self.filename.force_encoding("UTF-8")
79 79 end
80 80 if @temp_file.respond_to?(:content_type)
81 81 self.content_type = @temp_file.content_type.to_s.chomp
82 82 end
83 83 self.filesize = @temp_file.size
84 84 end
85 85 end
86 86 end
87 87
88 88 def file
89 89 nil
90 90 end
91 91
92 92 def filename=(arg)
93 93 write_attribute :filename, sanitize_filename(arg.to_s)
94 94 filename
95 95 end
96 96
97 97 # Copies the temporary file to its final location
98 98 # and computes its MD5 hash
99 99 def files_to_final_location
100 100 if @temp_file && (@temp_file.size > 0)
101 101 self.disk_directory = target_directory
102 102 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
103 103 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
104 104 path = File.dirname(diskfile)
105 105 unless File.directory?(path)
106 106 FileUtils.mkdir_p(path)
107 107 end
108 108 md5 = Digest::MD5.new
109 109 File.open(diskfile, "wb") do |f|
110 110 if @temp_file.respond_to?(:read)
111 111 buffer = ""
112 112 while (buffer = @temp_file.read(8192))
113 113 f.write(buffer)
114 114 md5.update(buffer)
115 115 end
116 116 else
117 117 f.write(@temp_file)
118 118 md5.update(@temp_file)
119 119 end
120 120 end
121 121 self.digest = md5.hexdigest
122 122 end
123 123 @temp_file = nil
124 124
125 125 if content_type.blank? && filename.present?
126 126 self.content_type = Redmine::MimeType.of(filename)
127 127 end
128 128 # Don't save the content type if it's longer than the authorized length
129 129 if self.content_type && self.content_type.length > 255
130 130 self.content_type = nil
131 131 end
132 132 end
133 133
134 134 # Deletes the file from the file system if it's not referenced by other attachments
135 135 def delete_from_disk
136 136 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
137 137 delete_from_disk!
138 138 end
139 139 end
140 140
141 141 # Returns file's location on disk
142 142 def diskfile
143 143 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
144 144 end
145 145
146 146 def title
147 147 title = filename.to_s
148 148 if description.present?
149 149 title << " (#{description})"
150 150 end
151 151 title
152 152 end
153 153
154 154 def increment_download
155 155 increment!(:downloads)
156 156 end
157 157
158 158 def project
159 159 container.try(:project)
160 160 end
161 161
162 162 def visible?(user=User.current)
163 163 if container_id
164 164 container && container.attachments_visible?(user)
165 165 else
166 166 author == user
167 167 end
168 168 end
169 169
170 170 def editable?(user=User.current)
171 171 if container_id
172 172 container && container.attachments_editable?(user)
173 173 else
174 174 author == user
175 175 end
176 176 end
177 177
178 178 def deletable?(user=User.current)
179 179 if container_id
180 180 container && container.attachments_deletable?(user)
181 181 else
182 182 author == user
183 183 end
184 184 end
185 185
186 186 def image?
187 187 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
188 188 end
189 189
190 190 def thumbnailable?
191 191 image?
192 192 end
193 193
194 194 # Returns the full path the attachment thumbnail, or nil
195 195 # if the thumbnail cannot be generated.
196 196 def thumbnail(options={})
197 197 if thumbnailable? && readable?
198 198 size = options[:size].to_i
199 199 if size > 0
200 200 # Limit the number of thumbnails per image
201 201 size = (size / 50) * 50
202 202 # Maximum thumbnail size
203 203 size = 800 if size > 800
204 204 else
205 205 size = Setting.thumbnails_size.to_i
206 206 end
207 207 size = 100 unless size > 0
208 208 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
209 209
210 210 begin
211 211 Redmine::Thumbnail.generate(self.diskfile, target, size)
212 212 rescue => e
213 213 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
214 214 return nil
215 215 end
216 216 end
217 217 end
218 218
219 219 # Deletes all thumbnails
220 220 def self.clear_thumbnails
221 221 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
222 222 File.delete file
223 223 end
224 224 end
225 225
226 226 def is_text?
227 227 Redmine::MimeType.is_type?('text', filename)
228 228 end
229 229
230 230 def is_diff?
231 231 self.filename =~ /\.(patch|diff)$/i
232 232 end
233 233
234 234 # Returns true if the file is readable
235 235 def readable?
236 236 File.readable?(diskfile)
237 237 end
238 238
239 239 # Returns the attachment token
240 240 def token
241 241 "#{id}.#{digest}"
242 242 end
243 243
244 244 # Finds an attachment that matches the given token and that has no container
245 245 def self.find_by_token(token)
246 246 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
247 247 attachment_id, attachment_digest = $1, $2
248 248 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
249 249 if attachment && attachment.container.nil?
250 250 attachment
251 251 end
252 252 end
253 253 end
254 254
255 255 # Bulk attaches a set of files to an object
256 256 #
257 257 # Returns a Hash of the results:
258 258 # :files => array of the attached files
259 259 # :unsaved => array of the files that could not be attached
260 260 def self.attach_files(obj, attachments)
261 261 result = obj.save_attachments(attachments, User.current)
262 262 obj.attach_saved_attachments
263 263 result
264 264 end
265 265
266 266 # Updates the filename and description of a set of attachments
267 267 # with the given hash of attributes. Returns true if all
268 268 # attachments were updated.
269 269 #
270 270 # Example:
271 271 # Attachment.update_attachments(attachments, {
272 272 # 4 => {:filename => 'foo'},
273 273 # 7 => {:filename => 'bar', :description => 'file description'}
274 274 # })
275 275 #
276 276 def self.update_attachments(attachments, params)
277 277 params = params.transform_keys {|key| key.to_i}
278 278
279 279 saved = true
280 280 transaction do
281 281 attachments.each do |attachment|
282 282 if p = params[attachment.id]
283 283 attachment.filename = p[:filename] if p.key?(:filename)
284 284 attachment.description = p[:description] if p.key?(:description)
285 285 saved &&= attachment.save
286 286 end
287 287 end
288 288 unless saved
289 289 raise ActiveRecord::Rollback
290 290 end
291 291 end
292 292 saved
293 293 end
294 294
295 295 def self.latest_attach(attachments, filename)
296 296 attachments.sort_by(&:created_on).reverse.detect do |att|
297 297 att.filename.downcase == filename.downcase
298 298 end
299 299 end
300 300
301 301 def self.prune(age=1.day)
302 302 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
303 303 end
304 304
305 305 # Moves an existing attachment to its target directory
306 306 def move_to_target_directory!
307 307 return unless !new_record? & readable?
308 308
309 309 src = diskfile
310 310 self.disk_directory = target_directory
311 311 dest = diskfile
312 312
313 313 return if src == dest
314 314
315 315 if !FileUtils.mkdir_p(File.dirname(dest))
316 316 logger.error "Could not create directory #{File.dirname(dest)}" if logger
317 317 return
318 318 end
319 319
320 320 if !FileUtils.mv(src, dest)
321 321 logger.error "Could not move attachment from #{src} to #{dest}" if logger
322 322 return
323 323 end
324 324
325 325 update_column :disk_directory, disk_directory
326 326 end
327 327
328 328 # Moves existing attachments that are stored at the root of the files
329 329 # directory (ie. created before Redmine 2.3) to their target subdirectories
330 330 def self.move_from_root_to_target_directory
331 331 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
332 332 attachment.move_to_target_directory!
333 333 end
334 334 end
335 335
336 336 private
337 337
338 338 # Physically deletes the file from the file system
339 339 def delete_from_disk!
340 340 if disk_filename.present? && File.exist?(diskfile)
341 341 File.delete(diskfile)
342 342 end
343 343 end
344 344
345 345 def sanitize_filename(value)
346 346 # get only the filename, not the whole path
347 347 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
348 348
349 349 # Finally, replace invalid characters with underscore
350 350 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
351 351 end
352 352
353 353 # Returns the subdirectory in which the attachment will be saved
354 354 def target_directory
355 355 time = created_on || DateTime.now
356 356 time.strftime("%Y/%m")
357 357 end
358 358
359 359 # Returns an ASCII or hashed filename that do not
360 360 # exists yet in the given subdirectory
361 361 def self.disk_filename(filename, directory=nil)
362 362 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
363 363 ascii = ''
364 364 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
365 365 ascii = filename
366 366 else
367 367 ascii = Digest::MD5.hexdigest(filename)
368 368 # keep the extension if any
369 369 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
370 370 end
371 371 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
372 372 timestamp.succ!
373 373 end
374 374 "#{timestamp}_#{ascii}"
375 375 end
376 376 end
@@ -1,93 +1,93
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 # Generic exception for when the AuthSource can not be reached
19 19 # (eg. can not connect to the LDAP)
20 20 class AuthSourceException < Exception; end
21 21 class AuthSourceTimeoutException < AuthSourceException; end
22 22
23 23 class AuthSource < ActiveRecord::Base
24 24 include Redmine::SubclassFactory
25 25 include Redmine::Ciphering
26 26
27 27 has_many :users
28 28
29 29 validates_presence_of :name
30 30 validates_uniqueness_of :name
31 31 validates_length_of :name, :maximum => 60
32 32 attr_protected :id
33 33
34 34 def authenticate(login, password)
35 35 end
36 36
37 37 def test_connection
38 38 end
39 39
40 40 def auth_method_name
41 41 "Abstract"
42 42 end
43 43
44 44 def account_password
45 45 read_ciphered_attribute(:account_password)
46 46 end
47 47
48 48 def account_password=(arg)
49 49 write_ciphered_attribute(:account_password, arg)
50 50 end
51 51
52 52 def searchable?
53 53 false
54 54 end
55 55
56 56 def self.search(q)
57 57 results = []
58 58 AuthSource.all.each do |source|
59 59 begin
60 60 if source.searchable?
61 61 results += source.search(q)
62 62 end
63 63 rescue AuthSourceException => e
64 64 logger.error "Error while searching users in #{source.name}: #{e.message}"
65 65 end
66 66 end
67 67 results
68 68 end
69 69
70 70 def allow_password_changes?
71 71 self.class.allow_password_changes?
72 72 end
73 73
74 74 # Does this auth source backend allow password changes?
75 75 def self.allow_password_changes?
76 76 false
77 77 end
78 78
79 79 # Try to authenticate a user not yet registered against available sources
80 80 def self.authenticate(login, password)
81 81 AuthSource.where(:onthefly_register => true).each do |source|
82 82 begin
83 83 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
84 84 attrs = source.authenticate(login, password)
85 85 rescue => e
86 86 logger.error "Error during authentication: #{e.message}"
87 87 attrs = nil
88 88 end
89 89 return attrs if attrs
90 90 end
91 91 return nil
92 92 end
93 93 end
@@ -1,196 +1,196
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 ldap_con.search( :base => self.base_dn,
179 179 :filter => search_filter,
180 180 :attributes=> search_attributes) do |entry|
181 181 if onthefly_register?
182 182 attrs = get_user_attributes_from_ldap_entry(entry)
183 183 else
184 184 attrs = {:dn => entry.dn}
185 185 end
186 186 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
187 187 end
188 188 attrs
189 189 end
190 190
191 191 def self.get_attr(entry, attr_name)
192 192 if !attr_name.blank?
193 193 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
194 194 end
195 195 end
196 196 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Board < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 has_many :messages, lambda {order("#{Message.table_name}.created_on DESC")}, :dependent => :destroy
22 22 belongs_to :last_message, :class_name => 'Message'
23 23 acts_as_tree :dependent => :nullify
24 24 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
25 25 acts_as_watchable
26 26
27 27 validates_presence_of :name, :description
28 28 validates_length_of :name, :maximum => 30
29 29 validates_length_of :description, :maximum => 255
30 30 validate :validate_board
31 31 attr_protected :id
32 32
33 33 scope :visible, lambda {|*args|
34 34 joins(:project).
35 35 where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
36 36 }
37 37
38 38 safe_attributes 'name', 'description', 'parent_id', 'move_to'
39 39
40 40 def visible?(user=User.current)
41 41 !user.nil? && user.allowed_to?(:view_messages, project)
42 42 end
43 43
44 44 def reload(*args)
45 45 @valid_parents = nil
46 46 super
47 47 end
48 48
49 49 def to_s
50 50 name
51 51 end
52 52
53 53 # Returns a scope for the board topics (messages without parent)
54 54 def topics
55 55 messages.where(:parent_id => nil)
56 56 end
57 57
58 58 def valid_parents
59 59 @valid_parents ||= project.boards - self_and_descendants
60 60 end
61 61
62 62 def reset_counters!
63 63 self.class.reset_counters!(id)
64 64 end
65 65
66 66 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
67 67 def self.reset_counters!(board_id)
68 68 board_id = board_id.to_i
69 69 Board.where(:id => board_id).
70 70 update_all(["topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=:id AND parent_id IS NULL)," +
71 71 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=:id)," +
72 72 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=:id)", :id => board_id])
73 73 end
74 74
75 75 def self.board_tree(boards, parent_id=nil, level=0)
76 76 tree = []
77 77 boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
78 78 tree << [board, level]
79 79 tree += board_tree(boards, board.id, level+1)
80 80 end
81 81 if block_given?
82 82 tree.each do |board, level|
83 83 yield board, level
84 84 end
85 85 end
86 86 tree
87 87 end
88 88
89 89 protected
90 90
91 91 def validate_board
92 92 if parent_id && parent_id_changed?
93 93 errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
94 94 end
95 95 end
96 96 end
@@ -1,34 +1,34
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Change < ActiveRecord::Base
19 19 belongs_to :changeset
20 20
21 21 validates_presence_of :changeset_id, :action, :path
22 22 before_save :init_path
23 23 before_validation :replace_invalid_utf8_of_path
24 24 attr_protected :id
25 25
26 26 def replace_invalid_utf8_of_path
27 27 self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
28 28 self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
29 29 end
30 30
31 31 def init_path
32 32 self.path ||= ""
33 33 end
34 34 end
@@ -1,291 +1,291
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 belongs_to :user
21 21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
22 22 has_and_belongs_to_many :issues
23 23 has_and_belongs_to_many :parents,
24 24 :class_name => "Changeset",
25 25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
26 26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
27 27 has_and_belongs_to_many :children,
28 28 :class_name => "Changeset",
29 29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
30 30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
31 31
32 32 acts_as_event :title => Proc.new {|o| o.title},
33 33 :description => :long_comments,
34 34 :datetime => :committed_on,
35 35 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
36 36
37 37 acts_as_searchable :columns => 'comments',
38 38 :preload => {:repository => :project},
39 39 :project_key => "#{Repository.table_name}.project_id",
40 40 :date_column => :committed_on
41 41
42 42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
43 43 :author_key => :user_id,
44 44 :scope => preload(:user, {:repository => :project})
45 45
46 46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
47 47 validates_uniqueness_of :revision, :scope => :repository_id
48 48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
49 49 attr_protected :id
50 50
51 51 scope :visible, lambda {|*args|
52 52 joins(:repository => :project).
53 53 where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
54 54 }
55 55
56 56 after_create :scan_for_issues
57 57 before_create :before_create_cs
58 58
59 59 def revision=(r)
60 60 write_attribute :revision, (r.nil? ? nil : r.to_s)
61 61 end
62 62
63 63 # Returns the identifier of this changeset; depending on repository backends
64 64 def identifier
65 65 if repository.class.respond_to? :changeset_identifier
66 66 repository.class.changeset_identifier self
67 67 else
68 68 revision.to_s
69 69 end
70 70 end
71 71
72 72 def committed_on=(date)
73 73 self.commit_date = date
74 74 super
75 75 end
76 76
77 77 # Returns the readable identifier
78 78 def format_identifier
79 79 if repository.class.respond_to? :format_changeset_identifier
80 80 repository.class.format_changeset_identifier self
81 81 else
82 82 identifier
83 83 end
84 84 end
85 85
86 86 def project
87 87 repository.project
88 88 end
89 89
90 90 def author
91 91 user || committer.to_s.split('<').first
92 92 end
93 93
94 94 def before_create_cs
95 95 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
96 96 self.comments = self.class.normalize_comments(
97 97 self.comments, repository.repo_log_encoding)
98 98 self.user = repository.find_committer_user(self.committer)
99 99 end
100 100
101 101 def scan_for_issues
102 102 scan_comment_for_issue_ids
103 103 end
104 104
105 105 TIMELOG_RE = /
106 106 (
107 107 ((\d+)(h|hours?))((\d+)(m|min)?)?
108 108 |
109 109 ((\d+)(h|hours?|m|min))
110 110 |
111 111 (\d+):(\d+)
112 112 |
113 113 (\d+([\.,]\d+)?)h?
114 114 )
115 115 /x
116 116
117 117 def scan_comment_for_issue_ids
118 118 return if comments.blank?
119 119 # keywords used to reference issues
120 120 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
121 121 ref_keywords_any = ref_keywords.delete('*')
122 122 # keywords used to fix issues
123 123 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
124 124
125 125 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
126 126
127 127 referenced_issues = []
128 128
129 129 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
130 130 action, refs = match[2].to_s.downcase, match[3]
131 131 next unless action.present? || ref_keywords_any
132 132
133 133 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
134 134 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
135 135 if issue && !issue_linked_to_same_commit?(issue)
136 136 referenced_issues << issue
137 137 # Don't update issues or log time when importing old commits
138 138 unless repository.created_on && committed_on && committed_on < repository.created_on
139 139 fix_issue(issue, action) if fix_keywords.include?(action)
140 140 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
141 141 end
142 142 end
143 143 end
144 144 end
145 145
146 146 referenced_issues.uniq!
147 147 self.issues = referenced_issues unless referenced_issues.empty?
148 148 end
149 149
150 150 def short_comments
151 151 @short_comments || split_comments.first
152 152 end
153 153
154 154 def long_comments
155 155 @long_comments || split_comments.last
156 156 end
157 157
158 158 def text_tag(ref_project=nil)
159 159 repo = ""
160 160 if repository && repository.identifier.present?
161 161 repo = "#{repository.identifier}|"
162 162 end
163 163 tag = if scmid?
164 164 "commit:#{repo}#{scmid}"
165 165 else
166 166 "#{repo}r#{revision}"
167 167 end
168 168 if ref_project && project && ref_project != project
169 169 tag = "#{project.identifier}:#{tag}"
170 170 end
171 171 tag
172 172 end
173 173
174 174 # Returns the title used for the changeset in the activity/search results
175 175 def title
176 176 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
177 177 comm = short_comments.blank? ? '' : (': ' + short_comments)
178 178 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
179 179 end
180 180
181 181 # Returns the previous changeset
182 182 def previous
183 183 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
184 184 end
185 185
186 186 # Returns the next changeset
187 187 def next
188 188 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
189 189 end
190 190
191 191 # Creates a new Change from it's common parameters
192 192 def create_change(change)
193 193 Change.create(:changeset => self,
194 194 :action => change[:action],
195 195 :path => change[:path],
196 196 :from_path => change[:from_path],
197 197 :from_revision => change[:from_revision])
198 198 end
199 199
200 200 # Finds an issue that can be referenced by the commit message
201 201 def find_referenced_issue_by_id(id)
202 202 return nil if id.blank?
203 203 issue = Issue.includes(:project).where(:id => id.to_i).first
204 204 if Setting.commit_cross_project_ref?
205 205 # all issues can be referenced/fixed
206 206 elsif issue
207 207 # issue that belong to the repository project, a subproject or a parent project only
208 208 unless issue.project &&
209 209 (project == issue.project || project.is_ancestor_of?(issue.project) ||
210 210 project.is_descendant_of?(issue.project))
211 211 issue = nil
212 212 end
213 213 end
214 214 issue
215 215 end
216 216
217 217 private
218 218
219 219 # Returns true if the issue is already linked to the same commit
220 220 # from a different repository
221 221 def issue_linked_to_same_commit?(issue)
222 222 repository.same_commits_in_scope(issue.changesets, self).any?
223 223 end
224 224
225 225 # Updates the +issue+ according to +action+
226 226 def fix_issue(issue, action)
227 227 # the issue may have been updated by the closure of another one (eg. duplicate)
228 228 issue.reload
229 229 # don't change the status is the issue is closed
230 230 return if issue.closed?
231 231
232 232 journal = issue.init_journal(user || User.anonymous,
233 233 ll(Setting.default_language,
234 234 :text_status_changed_by_changeset,
235 235 text_tag(issue.project)))
236 236 rule = Setting.commit_update_keywords_array.detect do |rule|
237 237 rule['keywords'].include?(action) &&
238 238 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
239 239 end
240 240 if rule
241 241 issue.assign_attributes rule.slice(*Issue.attribute_names)
242 242 end
243 243 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
244 244 { :changeset => self, :issue => issue, :action => action })
245 245 unless issue.save
246 246 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
247 247 end
248 248 issue
249 249 end
250 250
251 251 def log_time(issue, hours)
252 252 time_entry = TimeEntry.new(
253 253 :user => user,
254 254 :hours => hours,
255 255 :issue => issue,
256 256 :spent_on => commit_date,
257 257 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
258 258 :locale => Setting.default_language)
259 259 )
260 260 time_entry.activity = log_time_activity unless log_time_activity.nil?
261 261
262 262 unless time_entry.save
263 263 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
264 264 end
265 265 time_entry
266 266 end
267 267
268 268 def log_time_activity
269 269 if Setting.commit_logtime_activity_id.to_i > 0
270 270 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
271 271 end
272 272 end
273 273
274 274 def split_comments
275 275 comments =~ /\A(.+?)\r?\n(.*)$/m
276 276 @short_comments = $1 || comments
277 277 @long_comments = $2.to_s.strip
278 278 return @short_comments, @long_comments
279 279 end
280 280
281 281 public
282 282
283 283 # Strips and reencodes a commit log before insertion into the database
284 284 def self.normalize_comments(str, encoding)
285 285 Changeset.to_utf8(str.to_s.strip, encoding)
286 286 end
287 287
288 288 def self.to_utf8(str, encoding)
289 289 Redmine::CodesetUtil.to_utf8(str, encoding)
290 290 end
291 291 end
@@ -1,38 +1,38
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Comment < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :commented, :polymorphic => true, :counter_cache => true
21 21 belongs_to :author, :class_name => 'User'
22 22
23 23 validates_presence_of :commented, :author, :comments
24 24 attr_protected :id
25 25
26 26 after_create :send_notification
27 27
28 28 safe_attributes 'comments'
29 29
30 30 private
31 31
32 32 def send_notification
33 33 mailer_method = "#{commented.class.name.underscore}_comment_added"
34 34 if Setting.notified_events.include?(mailer_method)
35 35 Mailer.send(mailer_method, self).deliver
36 36 end
37 37 end
38 38 end
@@ -1,283 +1,283
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 attr_protected :id
33 33
34 34 before_validation :set_searchable
35 35 before_save do |field|
36 36 field.format.before_custom_field_save(field)
37 37 end
38 38 after_save :handle_multiplicity_change
39 39 after_save do |field|
40 40 if field.visible_changed? && field.visible
41 41 field.roles.clear
42 42 end
43 43 end
44 44
45 45 scope :sorted, lambda { order(:position) }
46 46 scope :visible, lambda {|*args|
47 47 user = args.shift || User.current
48 48 if user.admin?
49 49 # nop
50 50 elsif user.memberships.any?
51 51 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
52 52 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
53 53 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
54 54 " WHERE m.user_id = ?)",
55 55 true, user.id)
56 56 else
57 57 where(:visible => true)
58 58 end
59 59 }
60 60
61 61 def visible_by?(project, user=User.current)
62 62 visible? || user.admin?
63 63 end
64 64
65 65 def format
66 66 @format ||= Redmine::FieldFormat.find(field_format)
67 67 end
68 68
69 69 def field_format=(arg)
70 70 # cannot change format of a saved custom field
71 71 if new_record?
72 72 @format = nil
73 73 super
74 74 end
75 75 end
76 76
77 77 def set_searchable
78 78 # make sure these fields are not searchable
79 79 self.searchable = false unless format.class.searchable_supported
80 80 # make sure only these fields can have multiple values
81 81 self.multiple = false unless format.class.multiple_supported
82 82 true
83 83 end
84 84
85 85 def validate_custom_field
86 86 format.validate_custom_field(self).each do |attribute, message|
87 87 errors.add attribute, message
88 88 end
89 89
90 90 if regexp.present?
91 91 begin
92 92 Regexp.new(regexp)
93 93 rescue
94 94 errors.add(:regexp, :invalid)
95 95 end
96 96 end
97 97
98 98 if default_value.present?
99 99 validate_field_value(default_value).each do |message|
100 100 errors.add :default_value, message
101 101 end
102 102 end
103 103 end
104 104
105 105 def possible_custom_value_options(custom_value)
106 106 format.possible_custom_value_options(custom_value)
107 107 end
108 108
109 109 def possible_values_options(object=nil)
110 110 if object.is_a?(Array)
111 111 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
112 112 else
113 113 format.possible_values_options(self, object) || []
114 114 end
115 115 end
116 116
117 117 def possible_values
118 118 values = read_attribute(:possible_values)
119 119 if values.is_a?(Array)
120 120 values.each do |value|
121 121 value.force_encoding('UTF-8')
122 122 end
123 123 values
124 124 else
125 125 []
126 126 end
127 127 end
128 128
129 129 # Makes possible_values accept a multiline string
130 130 def possible_values=(arg)
131 131 if arg.is_a?(Array)
132 132 values = arg.compact.collect(&:strip).select {|v| !v.blank?}
133 133 write_attribute(:possible_values, values)
134 134 else
135 135 self.possible_values = arg.to_s.split(/[\n\r]+/)
136 136 end
137 137 end
138 138
139 139 def cast_value(value)
140 140 format.cast_value(self, value)
141 141 end
142 142
143 143 def value_from_keyword(keyword, customized)
144 144 possible_values_options = possible_values_options(customized)
145 145 if possible_values_options.present?
146 146 keyword = keyword.to_s.downcase
147 147 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
148 148 if v.is_a?(Array)
149 149 v.last
150 150 else
151 151 v
152 152 end
153 153 end
154 154 else
155 155 keyword
156 156 end
157 157 end
158 158
159 159 # Returns a ORDER BY clause that can used to sort customized
160 160 # objects by their value of the custom field.
161 161 # Returns nil if the custom field can not be used for sorting.
162 162 def order_statement
163 163 return nil if multiple?
164 164 format.order_statement(self)
165 165 end
166 166
167 167 # Returns a GROUP BY clause that can used to group by custom value
168 168 # Returns nil if the custom field can not be used for grouping.
169 169 def group_statement
170 170 return nil if multiple?
171 171 format.group_statement(self)
172 172 end
173 173
174 174 def join_for_order_statement
175 175 format.join_for_order_statement(self)
176 176 end
177 177
178 178 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
179 179 if visible? || user.admin?
180 180 "1=1"
181 181 elsif user.anonymous?
182 182 "1=0"
183 183 else
184 184 project_key ||= "#{self.class.customized_class.table_name}.project_id"
185 185 id_column ||= id
186 186 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
187 187 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
188 188 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
189 189 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
190 190 end
191 191 end
192 192
193 193 def self.visibility_condition
194 194 if user.admin?
195 195 "1=1"
196 196 elsif user.anonymous?
197 197 "#{table_name}.visible"
198 198 else
199 199 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
200 200 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
201 201 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
202 202 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
203 203 end
204 204 end
205 205
206 206 def <=>(field)
207 207 position <=> field.position
208 208 end
209 209
210 210 # Returns the class that values represent
211 211 def value_class
212 212 format.target_class if format.respond_to?(:target_class)
213 213 end
214 214
215 215 def self.customized_class
216 216 self.name =~ /^(.+)CustomField$/
217 217 $1.constantize rescue nil
218 218 end
219 219
220 220 # to move in project_custom_field
221 221 def self.for_all
222 222 where(:is_for_all => true).order('position').to_a
223 223 end
224 224
225 225 def type_name
226 226 nil
227 227 end
228 228
229 229 # Returns the error messages for the given value
230 230 # or an empty array if value is a valid value for the custom field
231 231 def validate_custom_value(custom_value)
232 232 value = custom_value.value
233 233 errs = []
234 234 if value.is_a?(Array)
235 235 if !multiple?
236 236 errs << ::I18n.t('activerecord.errors.messages.invalid')
237 237 end
238 238 if is_required? && value.detect(&:present?).nil?
239 239 errs << ::I18n.t('activerecord.errors.messages.blank')
240 240 end
241 241 else
242 242 if is_required? && value.blank?
243 243 errs << ::I18n.t('activerecord.errors.messages.blank')
244 244 end
245 245 end
246 246 errs += format.validate_custom_value(custom_value)
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(CustomFieldValue.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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomFieldValue
19 19 attr_accessor :custom_field, :customized, :value, :value_was
20 20
21 21 def initialize(attributes={})
22 22 attributes.each do |name, v|
23 23 send "#{name}=", v
24 24 end
25 25 end
26 26
27 27 def custom_field_id
28 28 custom_field.id
29 29 end
30 30
31 31 def true?
32 32 self.value == '1'
33 33 end
34 34
35 35 def editable?
36 36 custom_field.editable?
37 37 end
38 38
39 39 def visible?
40 40 custom_field.visible?
41 41 end
42 42
43 43 def required?
44 44 custom_field.is_required?
45 45 end
46 46
47 47 def to_s
48 48 value.to_s
49 49 end
50 50
51 51 def validate_value
52 52 custom_field.validate_custom_value(self).each do |message|
53 53 customized.errors.add(:base, custom_field.name + ' ' + message)
54 54 end
55 55 end
56 56 end
@@ -1,50 +1,50
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomValue < ActiveRecord::Base
19 19 belongs_to :custom_field
20 20 belongs_to :customized, :polymorphic => true
21 21 attr_protected :id
22 22
23 23 def initialize(attributes=nil, *args)
24 24 super
25 25 if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
26 26 self.value ||= custom_field.default_value
27 27 end
28 28 end
29 29
30 30 # Returns true if the boolean custom value is true
31 31 def true?
32 32 self.value == '1'
33 33 end
34 34
35 35 def editable?
36 36 custom_field.editable?
37 37 end
38 38
39 39 def visible?
40 40 custom_field.visible?
41 41 end
42 42
43 43 def required?
44 44 custom_field.is_required?
45 45 end
46 46
47 47 def to_s
48 48 value.to_s
49 49 end
50 50 end
@@ -1,70 +1,70
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Document < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :category, :class_name => "DocumentCategory"
22 22 acts_as_attachable :delete_permission => :delete_documents
23 23
24 24 acts_as_searchable :columns => ['title', "#{table_name}.description"],
25 25 :preload => :project
26 26 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
27 27 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
28 28 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
29 29 acts_as_activity_provider :scope => preload(:project)
30 30
31 31 validates_presence_of :project, :title, :category
32 32 validates_length_of :title, :maximum => 60
33 33 attr_protected :id
34 34
35 35 after_create :send_notification
36 36
37 37 scope :visible, lambda {|*args|
38 38 joins(:project).
39 39 where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
40 40 }
41 41
42 42 safe_attributes 'category_id', 'title', 'description'
43 43
44 44 def visible?(user=User.current)
45 45 !user.nil? && user.allowed_to?(:view_documents, project)
46 46 end
47 47
48 48 def initialize(attributes=nil, *args)
49 49 super
50 50 if new_record?
51 51 self.category ||= DocumentCategory.default
52 52 end
53 53 end
54 54
55 55 def updated_on
56 56 unless @updated_on
57 57 a = attachments.last
58 58 @updated_on = (a && a.created_on) || created_on
59 59 end
60 60 @updated_on
61 61 end
62 62
63 63 private
64 64
65 65 def send_notification
66 66 if Setting.notified_events.include?('document_added')
67 67 Mailer.document_added(self).deliver
68 68 end
69 69 end
70 70 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EnabledModule < ActiveRecord::Base
19 19 belongs_to :project
20 20 acts_as_watchable
21 21
22 22 validates_presence_of :name
23 23 validates_uniqueness_of :name, :scope => :project_id
24 24 attr_protected :id
25 25
26 26 after_create :module_enabled
27 27
28 28 private
29 29
30 30 # after_create callback used to do things when a module is enabled
31 31 def module_enabled
32 32 case name
33 33 when 'wiki'
34 34 # Create a wiki with a default start page
35 35 if project && project.wiki.nil?
36 36 Wiki.create(:project => project, :start_page => 'Wiki')
37 37 end
38 38 end
39 39 end
40 40 end
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Enumeration < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 default_scope lambda {order(:position)}
22 22
23 23 belongs_to :project
24 24
25 25 acts_as_list :scope => 'type = \'#{type}\''
26 26 acts_as_customizable
27 27 acts_as_tree
28 28
29 29 before_destroy :check_integrity
30 30 before_save :check_default
31 31
32 32 attr_protected :type
33 33
34 34 validates_presence_of :name
35 35 validates_uniqueness_of :name, :scope => [:type, :project_id]
36 36 validates_length_of :name, :maximum => 30
37 37
38 38 scope :shared, lambda { where(:project_id => nil) }
39 39 scope :sorted, lambda { order(:position) }
40 40 scope :active, lambda { where(:active => true) }
41 41 scope :system, lambda { where(:project_id => nil) }
42 42 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
43 43
44 44 def self.default
45 45 # Creates a fake default scope so Enumeration.default will check
46 46 # it's type. STI subclasses will automatically add their own
47 47 # types to the finder.
48 48 if self.descends_from_active_record?
49 49 where(:is_default => true, :type => 'Enumeration').first
50 50 else
51 51 # STI classes are
52 52 where(:is_default => true).first
53 53 end
54 54 end
55 55
56 56 # Overloaded on concrete classes
57 57 def option_name
58 58 nil
59 59 end
60 60
61 61 def check_default
62 62 if is_default? && is_default_changed?
63 63 Enumeration.where({:type => type}).update_all({:is_default => false})
64 64 end
65 65 end
66 66
67 67 # Overloaded on concrete classes
68 68 def objects_count
69 69 0
70 70 end
71 71
72 72 def in_use?
73 73 self.objects_count != 0
74 74 end
75 75
76 76 # Is this enumeration overriding a system level enumeration?
77 77 def is_override?
78 78 !self.parent.nil?
79 79 end
80 80
81 81 alias :destroy_without_reassign :destroy
82 82
83 83 # Destroy the enumeration
84 84 # If a enumeration is specified, objects are reassigned
85 85 def destroy(reassign_to = nil)
86 86 if reassign_to && reassign_to.is_a?(Enumeration)
87 87 self.transfer_relations(reassign_to)
88 88 end
89 89 destroy_without_reassign
90 90 end
91 91
92 92 def <=>(enumeration)
93 93 position <=> enumeration.position
94 94 end
95 95
96 96 def to_s; name end
97 97
98 98 # Returns the Subclasses of Enumeration. Each Subclass needs to be
99 99 # required in development mode.
100 100 #
101 101 # Note: subclasses is protected in ActiveRecord
102 102 def self.get_subclasses
103 103 subclasses
104 104 end
105 105
106 106 # Does the +new+ Hash override the previous Enumeration?
107 107 def self.overriding_change?(new, previous)
108 108 if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
109 109 return false
110 110 else
111 111 return true
112 112 end
113 113 end
114 114
115 115 # Does the +new+ Hash have the same custom values as the previous Enumeration?
116 116 def self.same_custom_values?(new, previous)
117 117 previous.custom_field_values.each do |custom_value|
118 118 if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
119 119 return false
120 120 end
121 121 end
122 122
123 123 return true
124 124 end
125 125
126 126 # Are the new and previous fields equal?
127 127 def self.same_active_state?(new, previous)
128 128 new = (new == "1" ? true : false)
129 129 return new == previous
130 130 end
131 131
132 132 private
133 133 def check_integrity
134 134 raise "Cannot 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,117 +1,117
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 attr_protected :id
32 32
33 33 before_destroy :remove_references_before_destroy
34 34
35 35 scope :sorted, lambda { order(:type => :asc, :lastname => :desc) }
36 36 scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
37 37 scope :givable, lambda {where(:type => 'Group')}
38 38
39 39 safe_attributes 'name',
40 40 'user_ids',
41 41 'custom_field_values',
42 42 'custom_fields',
43 43 :if => lambda {|group, user| user.admin? && !group.builtin?}
44 44
45 45 def to_s
46 46 name.to_s
47 47 end
48 48
49 49 def name
50 50 lastname
51 51 end
52 52
53 53 def name=(arg)
54 54 self.lastname = arg
55 55 end
56 56
57 57 def builtin_type
58 58 nil
59 59 end
60 60
61 61 # Return true if the group is a builtin group
62 62 def builtin?
63 63 false
64 64 end
65 65
66 66 # Returns true if the group can be given to a user
67 67 def givable?
68 68 !builtin?
69 69 end
70 70
71 71 def user_added(user)
72 72 members.each do |member|
73 73 next if member.project.nil?
74 74 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)
75 75 member.member_roles.each do |member_role|
76 76 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
77 77 end
78 78 user_member.save!
79 79 end
80 80 end
81 81
82 82 def user_removed(user)
83 83 members.each do |member|
84 84 MemberRole.
85 85 joins(:member).
86 86 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
87 87 each(&:destroy)
88 88 end
89 89 end
90 90
91 91 def self.human_attribute_name(attribute_key_name, *args)
92 92 attr_name = attribute_key_name.to_s
93 93 if attr_name == 'lastname'
94 94 attr_name = "name"
95 95 end
96 96 super(attr_name, *args)
97 97 end
98 98
99 99 def self.anonymous
100 100 GroupAnonymous.load_instance
101 101 end
102 102
103 103 def self.non_member
104 104 GroupNonMember.load_instance
105 105 end
106 106
107 107 private
108 108
109 109 # Removes references that are not handled by associations
110 110 def remove_references_before_destroy
111 111 return if self.id.nil?
112 112
113 113 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
114 114 end
115 115 end
116 116
117 117 require_dependency "group_builtin"
@@ -1,26 +1,26
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 GroupAnonymous < GroupBuiltin
19 19 def name
20 20 l(:label_group_anonymous)
21 21 end
22 22
23 23 def builtin_type
24 24 "anonymous"
25 25 end
26 26 end
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 GroupBuiltin < Group
19 19 validate :validate_uniqueness, :on => :create
20 20
21 21 def validate_uniqueness
22 22 errors.add :base, 'The builtin group already exists.' if self.class.exists?
23 23 end
24 24
25 25 def builtin?
26 26 true
27 27 end
28 28
29 29 def destroy
30 30 false
31 31 end
32 32
33 33 def user_added(user)
34 34 raise 'Cannot add users to a builtin group'
35 35 end
36 36
37 37 class << self
38 38 def load_instance
39 39 return nil if self == GroupBuiltin
40 40 instance = order('id').first || create_instance
41 41 end
42 42
43 43 def create_instance
44 44 raise 'The builtin group already exists.' if exists?
45 45 instance = new
46 46 instance.lastname = name
47 47 instance.save :validate => false
48 48 raise 'Unable to create builtin group.' if instance.new_record?
49 49 instance
50 50 end
51 51 private :create_instance
52 52 end
53 53 end
54 54
55 55 require_dependency "group_anonymous"
56 56 require_dependency "group_non_member"
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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,26 +1,26
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 GroupNonMember < GroupBuiltin
19 19 def name
20 20 l(:label_group_non_member)
21 21 end
22 22
23 23 def builtin_type
24 24 "non_member"
25 25 end
26 26 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
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was 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