##// END OF EJS Templates
Copyright for 2013 (#12788)....
Jean-Philippe Lang -
r10939:e396a0eebe07
parent child
Show More

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

@@ -1,298 +1,298
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
24 24
25 25 # Login request and validation
26 26 def login
27 27 if request.get?
28 28 if User.current.logged?
29 29 redirect_to home_url
30 30 end
31 31 else
32 32 authenticate_user
33 33 end
34 34 rescue AuthSourceException => e
35 35 logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
36 36 render_error :message => e.message
37 37 end
38 38
39 39 # Log out current user and redirect to welcome page
40 40 def logout
41 41 logout_user
42 42 redirect_to home_url
43 43 end
44 44
45 45 # Lets user choose a new password
46 46 def lost_password
47 47 redirect_to(home_url) && return unless Setting.lost_password?
48 48 if params[:token]
49 49 @token = Token.find_by_action_and_value("recovery", params[:token].to_s)
50 50 if @token.nil? || @token.expired?
51 51 redirect_to home_url
52 52 return
53 53 end
54 54 @user = @token.user
55 55 unless @user && @user.active?
56 56 redirect_to home_url
57 57 return
58 58 end
59 59 if request.post?
60 60 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
61 61 if @user.save
62 62 @token.destroy
63 63 flash[:notice] = l(:notice_account_password_updated)
64 64 redirect_to signin_path
65 65 return
66 66 end
67 67 end
68 68 render :template => "account/password_recovery"
69 69 return
70 70 else
71 71 if request.post?
72 72 user = User.find_by_mail(params[:mail].to_s)
73 73 # user not found or not active
74 74 unless user && user.active?
75 75 flash.now[:error] = l(:notice_account_unknown_email)
76 76 return
77 77 end
78 78 # user cannot change its password
79 79 unless user.change_password_allowed?
80 80 flash.now[:error] = l(:notice_can_t_change_password)
81 81 return
82 82 end
83 83 # create a new token for password recovery
84 84 token = Token.new(:user => user, :action => "recovery")
85 85 if token.save
86 86 Mailer.lost_password(token).deliver
87 87 flash[:notice] = l(:notice_account_lost_email_sent)
88 88 redirect_to signin_path
89 89 return
90 90 end
91 91 end
92 92 end
93 93 end
94 94
95 95 # User self-registration
96 96 def register
97 97 redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
98 98 if request.get?
99 99 session[:auth_source_registration] = nil
100 100 @user = User.new(:language => current_language.to_s)
101 101 else
102 102 user_params = params[:user] || {}
103 103 @user = User.new
104 104 @user.safe_attributes = user_params
105 105 @user.admin = false
106 106 @user.register
107 107 if session[:auth_source_registration]
108 108 @user.activate
109 109 @user.login = session[:auth_source_registration][:login]
110 110 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
111 111 if @user.save
112 112 session[:auth_source_registration] = nil
113 113 self.logged_user = @user
114 114 flash[:notice] = l(:notice_account_activated)
115 115 redirect_to my_account_path
116 116 end
117 117 else
118 118 @user.login = params[:user][:login]
119 119 unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
120 120 @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
121 121 end
122 122
123 123 case Setting.self_registration
124 124 when '1'
125 125 register_by_email_activation(@user)
126 126 when '3'
127 127 register_automatically(@user)
128 128 else
129 129 register_manually_by_administrator(@user)
130 130 end
131 131 end
132 132 end
133 133 end
134 134
135 135 # Token based account activation
136 136 def activate
137 137 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
138 138 token = Token.find_by_action_and_value('register', params[:token])
139 139 redirect_to(home_url) && return unless token and !token.expired?
140 140 user = token.user
141 141 redirect_to(home_url) && return unless user.registered?
142 142 user.activate
143 143 if user.save
144 144 token.destroy
145 145 flash[:notice] = l(:notice_account_activated)
146 146 end
147 147 redirect_to signin_path
148 148 end
149 149
150 150 private
151 151
152 152 def authenticate_user
153 153 if Setting.openid? && using_open_id?
154 154 open_id_authenticate(params[:openid_url])
155 155 else
156 156 password_authentication
157 157 end
158 158 end
159 159
160 160 def password_authentication
161 161 user = User.try_to_login(params[:username], params[:password])
162 162
163 163 if user.nil?
164 164 invalid_credentials
165 165 elsif user.new_record?
166 166 onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
167 167 else
168 168 # Valid user
169 169 successful_authentication(user)
170 170 end
171 171 end
172 172
173 173 def open_id_authenticate(openid_url)
174 174 authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration|
175 175 if result.successful?
176 176 user = User.find_or_initialize_by_identity_url(identity_url)
177 177 if user.new_record?
178 178 # Self-registration off
179 179 redirect_to(home_url) && return unless Setting.self_registration?
180 180
181 181 # Create on the fly
182 182 user.login = registration['nickname'] unless registration['nickname'].nil?
183 183 user.mail = registration['email'] unless registration['email'].nil?
184 184 user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
185 185 user.random_password
186 186 user.register
187 187
188 188 case Setting.self_registration
189 189 when '1'
190 190 register_by_email_activation(user) do
191 191 onthefly_creation_failed(user)
192 192 end
193 193 when '3'
194 194 register_automatically(user) do
195 195 onthefly_creation_failed(user)
196 196 end
197 197 else
198 198 register_manually_by_administrator(user) do
199 199 onthefly_creation_failed(user)
200 200 end
201 201 end
202 202 else
203 203 # Existing record
204 204 if user.active?
205 205 successful_authentication(user)
206 206 else
207 207 account_pending
208 208 end
209 209 end
210 210 end
211 211 end
212 212 end
213 213
214 214 def successful_authentication(user)
215 215 logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
216 216 # Valid user
217 217 self.logged_user = user
218 218 # generate a key and set cookie if autologin
219 219 if params[:autologin] && Setting.autologin?
220 220 set_autologin_cookie(user)
221 221 end
222 222 call_hook(:controller_account_success_authentication_after, {:user => user })
223 223 redirect_back_or_default my_page_path
224 224 end
225 225
226 226 def set_autologin_cookie(user)
227 227 token = Token.create(:user => user, :action => 'autologin')
228 228 cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin'
229 229 cookie_options = {
230 230 :value => token.value,
231 231 :expires => 1.year.from_now,
232 232 :path => (Redmine::Configuration['autologin_cookie_path'] || '/'),
233 233 :secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
234 234 :httponly => true
235 235 }
236 236 cookies[cookie_name] = cookie_options
237 237 end
238 238
239 239 # Onthefly creation failed, display the registration form to fill/fix attributes
240 240 def onthefly_creation_failed(user, auth_source_options = { })
241 241 @user = user
242 242 session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
243 243 render :action => 'register'
244 244 end
245 245
246 246 def invalid_credentials
247 247 logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
248 248 flash.now[:error] = l(:notice_account_invalid_creditentials)
249 249 end
250 250
251 251 # Register a user for email activation.
252 252 #
253 253 # Pass a block for behavior when a user fails to save
254 254 def register_by_email_activation(user, &block)
255 255 token = Token.new(:user => user, :action => "register")
256 256 if user.save and token.save
257 257 Mailer.register(token).deliver
258 258 flash[:notice] = l(:notice_account_register_done)
259 259 redirect_to signin_path
260 260 else
261 261 yield if block_given?
262 262 end
263 263 end
264 264
265 265 # Automatically register a user
266 266 #
267 267 # Pass a block for behavior when a user fails to save
268 268 def register_automatically(user, &block)
269 269 # Automatic activation
270 270 user.activate
271 271 user.last_login_on = Time.now
272 272 if user.save
273 273 self.logged_user = user
274 274 flash[:notice] = l(:notice_account_activated)
275 275 redirect_to my_account_path
276 276 else
277 277 yield if block_given?
278 278 end
279 279 end
280 280
281 281 # Manual activation by the administrator
282 282 #
283 283 # Pass a block for behavior when a user fails to save
284 284 def register_manually_by_administrator(user, &block)
285 285 if user.save
286 286 # Sends an email to the administrators
287 287 Mailer.account_activation_request(user).deliver
288 288 account_pending
289 289 else
290 290 yield if block_given?
291 291 end
292 292 end
293 293
294 294 def account_pending
295 295 flash[:notice] = l(:notice_account_pending)
296 296 redirect_to signin_path
297 297 end
298 298 end
@@ -1,75 +1,75
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ActivitiesController < ApplicationController
19 19 menu_item :activity
20 20 before_filter :find_optional_project
21 21 accept_rss_auth :index
22 22
23 23 def index
24 24 @days = Setting.activity_days_default.to_i
25 25
26 26 if params[:from]
27 27 begin; @date_to = params[:from].to_date + 1; rescue; end
28 28 end
29 29
30 30 @date_to ||= Date.today + 1
31 31 @date_from = @date_to - @days
32 32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
33 33 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
34 34
35 35 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
36 36 :with_subprojects => @with_subprojects,
37 37 :author => @author)
38 38 @activity.scope_select {|t| !params["show_#{t}"].nil?}
39 39 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
40 40
41 41 events = @activity.events(@date_from, @date_to)
42 42
43 43 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
44 44 respond_to do |format|
45 45 format.html {
46 46 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
47 47 render :layout => false if request.xhr?
48 48 }
49 49 format.atom {
50 50 title = l(:label_activity)
51 51 if @author
52 52 title = @author.name
53 53 elsif @activity.scope.size == 1
54 54 title = l("label_#{@activity.scope.first.singularize}_plural")
55 55 end
56 56 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
57 57 }
58 58 end
59 59 end
60 60
61 61 rescue ActiveRecord::RecordNotFound
62 62 render_404
63 63 end
64 64
65 65 private
66 66
67 67 # TODO: refactor, duplicated in projects_controller
68 68 def find_optional_project
69 69 return true unless params[:id]
70 70 @project = Project.find(params[:id])
71 71 authorize
72 72 rescue ActiveRecord::RecordNotFound
73 73 render_404
74 74 end
75 75 end
@@ -1,83 +1,83
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AdminController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :projects, :only => :projects
21 21 menu_item :plugins, :only => :plugins
22 22 menu_item :info, :only => :info
23 23
24 24 before_filter :require_admin
25 25 helper :sort
26 26 include SortHelper
27 27
28 28 def index
29 29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 30 end
31 31
32 32 def projects
33 33 @status = params[:status] || 1
34 34
35 35 scope = Project.status(@status).order('lft')
36 36 scope = scope.like(params[:name]) if params[:name].present?
37 37 @projects = scope.all
38 38
39 39 render :action => "projects", :layout => false if request.xhr?
40 40 end
41 41
42 42 def plugins
43 43 @plugins = Redmine::Plugin.all
44 44 end
45 45
46 46 # Loads the default configuration
47 47 # (roles, trackers, statuses, workflow, enumerations)
48 48 def default_configuration
49 49 if request.post?
50 50 begin
51 51 Redmine::DefaultData::Loader::load(params[:lang])
52 52 flash[:notice] = l(:notice_default_data_loaded)
53 53 rescue Exception => e
54 54 flash[:error] = l(:error_can_t_load_default_data, e.message)
55 55 end
56 56 end
57 57 redirect_to admin_path
58 58 end
59 59
60 60 def test_email
61 61 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
62 62 # Force ActionMailer to raise delivery errors so we can catch it
63 63 ActionMailer::Base.raise_delivery_errors = true
64 64 begin
65 65 @test = Mailer.test_email(User.current).deliver
66 66 flash[:notice] = l(:notice_email_sent, User.current.mail)
67 67 rescue Exception => e
68 68 flash[:error] = l(:notice_email_error, e.message)
69 69 end
70 70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
71 71 redirect_to settings_path(:tab => 'notifications')
72 72 end
73 73
74 74 def info
75 75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
76 76 @checklist = [
77 77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
78 78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
79 79 [:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
80 80 [:text_rmagick_available, Object.const_defined?(:Magick)]
81 81 ]
82 82 end
83 83 end
@@ -1,603 +1,603
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 def handle_unverified_request
37 37 super
38 38 cookies.delete(:autologin)
39 39 end
40 40
41 41 before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
42 42
43 43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44 44 rescue_from ::Unauthorized, :with => :deny_access
45 45 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
46 46
47 47 include Redmine::Search::Controller
48 48 include Redmine::MenuManager::MenuController
49 49 helper Redmine::MenuManager::MenuHelper
50 50
51 51 def session_expiration
52 52 if session[:user_id]
53 53 if session_expired? && !try_to_autologin
54 54 reset_session
55 55 flash[:error] = l(:error_session_expired)
56 56 redirect_to signin_url
57 57 else
58 58 session[:atime] = Time.now.utc.to_i
59 59 end
60 60 end
61 61 end
62 62
63 63 def session_expired?
64 64 if Setting.session_lifetime?
65 65 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
66 66 return true
67 67 end
68 68 end
69 69 if Setting.session_timeout?
70 70 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
71 71 return true
72 72 end
73 73 end
74 74 false
75 75 end
76 76
77 77 def start_user_session(user)
78 78 session[:user_id] = user.id
79 79 session[:ctime] = Time.now.utc.to_i
80 80 session[:atime] = Time.now.utc.to_i
81 81 end
82 82
83 83 def user_setup
84 84 # Check the settings cache for each request
85 85 Setting.check_cache
86 86 # Find the current user
87 87 User.current = find_current_user
88 88 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
89 89 end
90 90
91 91 # Returns the current user or nil if no user is logged in
92 92 # and starts a session if needed
93 93 def find_current_user
94 94 user = nil
95 95 unless api_request?
96 96 if session[:user_id]
97 97 # existing session
98 98 user = (User.active.find(session[:user_id]) rescue nil)
99 99 elsif autologin_user = try_to_autologin
100 100 user = autologin_user
101 101 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
102 102 # RSS key authentication does not start a session
103 103 user = User.find_by_rss_key(params[:key])
104 104 end
105 105 end
106 106 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
107 107 if (key = api_key_from_request)
108 108 # Use API key
109 109 user = User.find_by_api_key(key)
110 110 else
111 111 # HTTP Basic, either username/password or API key/random
112 112 authenticate_with_http_basic do |username, password|
113 113 user = User.try_to_login(username, password) || User.find_by_api_key(username)
114 114 end
115 115 end
116 116 # Switch user if requested by an admin user
117 117 if user && user.admin? && (username = api_switch_user_from_request)
118 118 su = User.find_by_login(username)
119 119 if su && su.active?
120 120 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
121 121 user = su
122 122 else
123 123 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
124 124 end
125 125 end
126 126 end
127 127 user
128 128 end
129 129
130 130 def try_to_autologin
131 131 if cookies[:autologin] && Setting.autologin?
132 132 # auto-login feature starts a new session
133 133 user = User.try_to_autologin(cookies[:autologin])
134 134 if user
135 135 reset_session
136 136 start_user_session(user)
137 137 end
138 138 user
139 139 end
140 140 end
141 141
142 142 # Sets the logged in user
143 143 def logged_user=(user)
144 144 reset_session
145 145 if user && user.is_a?(User)
146 146 User.current = user
147 147 start_user_session(user)
148 148 else
149 149 User.current = User.anonymous
150 150 end
151 151 end
152 152
153 153 # Logs out current user
154 154 def logout_user
155 155 if User.current.logged?
156 156 cookies.delete :autologin
157 157 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
158 158 self.logged_user = nil
159 159 end
160 160 end
161 161
162 162 # check if login is globally required to access the application
163 163 def check_if_login_required
164 164 # no check needed if user is already logged in
165 165 return true if User.current.logged?
166 166 require_login if Setting.login_required?
167 167 end
168 168
169 169 def set_localization
170 170 lang = nil
171 171 if User.current.logged?
172 172 lang = find_language(User.current.language)
173 173 end
174 174 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
175 175 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
176 176 if !accept_lang.blank?
177 177 accept_lang = accept_lang.downcase
178 178 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
179 179 end
180 180 end
181 181 lang ||= Setting.default_language
182 182 set_language_if_valid(lang)
183 183 end
184 184
185 185 def require_login
186 186 if !User.current.logged?
187 187 # Extract only the basic url parameters on non-GET requests
188 188 if request.get?
189 189 url = url_for(params)
190 190 else
191 191 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
192 192 end
193 193 respond_to do |format|
194 194 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
195 195 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
196 196 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
197 197 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
198 198 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
199 199 end
200 200 return false
201 201 end
202 202 true
203 203 end
204 204
205 205 def require_admin
206 206 return unless require_login
207 207 if !User.current.admin?
208 208 render_403
209 209 return false
210 210 end
211 211 true
212 212 end
213 213
214 214 def deny_access
215 215 User.current.logged? ? render_403 : require_login
216 216 end
217 217
218 218 # Authorize the user for the requested action
219 219 def authorize(ctrl = params[:controller], action = params[:action], global = false)
220 220 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
221 221 if allowed
222 222 true
223 223 else
224 224 if @project && @project.archived?
225 225 render_403 :message => :notice_not_authorized_archived_project
226 226 else
227 227 deny_access
228 228 end
229 229 end
230 230 end
231 231
232 232 # Authorize the user for the requested action outside a project
233 233 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
234 234 authorize(ctrl, action, global)
235 235 end
236 236
237 237 # Find project of id params[:id]
238 238 def find_project
239 239 @project = Project.find(params[:id])
240 240 rescue ActiveRecord::RecordNotFound
241 241 render_404
242 242 end
243 243
244 244 # Find project of id params[:project_id]
245 245 def find_project_by_project_id
246 246 @project = Project.find(params[:project_id])
247 247 rescue ActiveRecord::RecordNotFound
248 248 render_404
249 249 end
250 250
251 251 # Find a project based on params[:project_id]
252 252 # TODO: some subclasses override this, see about merging their logic
253 253 def find_optional_project
254 254 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
255 255 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
256 256 allowed ? true : deny_access
257 257 rescue ActiveRecord::RecordNotFound
258 258 render_404
259 259 end
260 260
261 261 # Finds and sets @project based on @object.project
262 262 def find_project_from_association
263 263 render_404 unless @object.present?
264 264
265 265 @project = @object.project
266 266 end
267 267
268 268 def find_model_object
269 269 model = self.class.model_object
270 270 if model
271 271 @object = model.find(params[:id])
272 272 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
273 273 end
274 274 rescue ActiveRecord::RecordNotFound
275 275 render_404
276 276 end
277 277
278 278 def self.model_object(model)
279 279 self.model_object = model
280 280 end
281 281
282 282 # Find the issue whose id is the :id parameter
283 283 # Raises a Unauthorized exception if the issue is not visible
284 284 def find_issue
285 285 # Issue.visible.find(...) can not be used to redirect user to the login form
286 286 # if the issue actually exists but requires authentication
287 287 @issue = Issue.find(params[:id])
288 288 raise Unauthorized unless @issue.visible?
289 289 @project = @issue.project
290 290 rescue ActiveRecord::RecordNotFound
291 291 render_404
292 292 end
293 293
294 294 # Find issues with a single :id param or :ids array param
295 295 # Raises a Unauthorized exception if one of the issues is not visible
296 296 def find_issues
297 297 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
298 298 raise ActiveRecord::RecordNotFound if @issues.empty?
299 299 raise Unauthorized unless @issues.all?(&:visible?)
300 300 @projects = @issues.collect(&:project).compact.uniq
301 301 @project = @projects.first if @projects.size == 1
302 302 rescue ActiveRecord::RecordNotFound
303 303 render_404
304 304 end
305 305
306 306 def find_attachments
307 307 if (attachments = params[:attachments]).present?
308 308 att = attachments.values.collect do |attachment|
309 309 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
310 310 end
311 311 att.compact!
312 312 end
313 313 @attachments = att || []
314 314 end
315 315
316 316 # make sure that the user is a member of the project (or admin) if project is private
317 317 # used as a before_filter for actions that do not require any particular permission on the project
318 318 def check_project_privacy
319 319 if @project && !@project.archived?
320 320 if @project.visible?
321 321 true
322 322 else
323 323 deny_access
324 324 end
325 325 else
326 326 @project = nil
327 327 render_404
328 328 false
329 329 end
330 330 end
331 331
332 332 def back_url
333 333 url = params[:back_url]
334 334 if url.nil? && referer = request.env['HTTP_REFERER']
335 335 url = CGI.unescape(referer.to_s)
336 336 end
337 337 url
338 338 end
339 339
340 340 def redirect_back_or_default(default)
341 341 back_url = params[:back_url].to_s
342 342 if back_url.present?
343 343 begin
344 344 uri = URI.parse(back_url)
345 345 # do not redirect user to another host or to the login or register page
346 346 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
347 347 redirect_to(back_url)
348 348 return
349 349 end
350 350 rescue URI::InvalidURIError
351 351 logger.warn("Could not redirect to invalid URL #{back_url}")
352 352 # redirect to default
353 353 end
354 354 end
355 355 redirect_to default
356 356 false
357 357 end
358 358
359 359 # Redirects to the request referer if present, redirects to args or call block otherwise.
360 360 def redirect_to_referer_or(*args, &block)
361 361 redirect_to :back
362 362 rescue ::ActionController::RedirectBackError
363 363 if args.any?
364 364 redirect_to *args
365 365 elsif block_given?
366 366 block.call
367 367 else
368 368 raise "#redirect_to_referer_or takes arguments or a block"
369 369 end
370 370 end
371 371
372 372 def render_403(options={})
373 373 @project = nil
374 374 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
375 375 return false
376 376 end
377 377
378 378 def render_404(options={})
379 379 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
380 380 return false
381 381 end
382 382
383 383 # Renders an error response
384 384 def render_error(arg)
385 385 arg = {:message => arg} unless arg.is_a?(Hash)
386 386
387 387 @message = arg[:message]
388 388 @message = l(@message) if @message.is_a?(Symbol)
389 389 @status = arg[:status] || 500
390 390
391 391 respond_to do |format|
392 392 format.html {
393 393 render :template => 'common/error', :layout => use_layout, :status => @status
394 394 }
395 395 format.any { head @status }
396 396 end
397 397 end
398 398
399 399 # Handler for ActionView::MissingTemplate exception
400 400 def missing_template
401 401 logger.warn "Missing template, responding with 404"
402 402 @project = nil
403 403 render_404
404 404 end
405 405
406 406 # Filter for actions that provide an API response
407 407 # but have no HTML representation for non admin users
408 408 def require_admin_or_api_request
409 409 return true if api_request?
410 410 if User.current.admin?
411 411 true
412 412 elsif User.current.logged?
413 413 render_error(:status => 406)
414 414 else
415 415 deny_access
416 416 end
417 417 end
418 418
419 419 # Picks which layout to use based on the request
420 420 #
421 421 # @return [boolean, string] name of the layout to use or false for no layout
422 422 def use_layout
423 423 request.xhr? ? false : 'base'
424 424 end
425 425
426 426 def invalid_authenticity_token
427 427 if api_request?
428 428 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
429 429 end
430 430 render_error "Invalid form authenticity token."
431 431 end
432 432
433 433 def render_feed(items, options={})
434 434 @items = items || []
435 435 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
436 436 @items = @items.slice(0, Setting.feeds_limit.to_i)
437 437 @title = options[:title] || Setting.app_title
438 438 render :template => "common/feed", :formats => [:atom], :layout => false,
439 439 :content_type => 'application/atom+xml'
440 440 end
441 441
442 442 def self.accept_rss_auth(*actions)
443 443 if actions.any?
444 444 self.accept_rss_auth_actions = actions
445 445 else
446 446 self.accept_rss_auth_actions || []
447 447 end
448 448 end
449 449
450 450 def accept_rss_auth?(action=action_name)
451 451 self.class.accept_rss_auth.include?(action.to_sym)
452 452 end
453 453
454 454 def self.accept_api_auth(*actions)
455 455 if actions.any?
456 456 self.accept_api_auth_actions = actions
457 457 else
458 458 self.accept_api_auth_actions || []
459 459 end
460 460 end
461 461
462 462 def accept_api_auth?(action=action_name)
463 463 self.class.accept_api_auth.include?(action.to_sym)
464 464 end
465 465
466 466 # Returns the number of objects that should be displayed
467 467 # on the paginated list
468 468 def per_page_option
469 469 per_page = nil
470 470 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
471 471 per_page = params[:per_page].to_s.to_i
472 472 session[:per_page] = per_page
473 473 elsif session[:per_page]
474 474 per_page = session[:per_page]
475 475 else
476 476 per_page = Setting.per_page_options_array.first || 25
477 477 end
478 478 per_page
479 479 end
480 480
481 481 # Returns offset and limit used to retrieve objects
482 482 # for an API response based on offset, limit and page parameters
483 483 def api_offset_and_limit(options=params)
484 484 if options[:offset].present?
485 485 offset = options[:offset].to_i
486 486 if offset < 0
487 487 offset = 0
488 488 end
489 489 end
490 490 limit = options[:limit].to_i
491 491 if limit < 1
492 492 limit = 25
493 493 elsif limit > 100
494 494 limit = 100
495 495 end
496 496 if offset.nil? && options[:page].present?
497 497 offset = (options[:page].to_i - 1) * limit
498 498 offset = 0 if offset < 0
499 499 end
500 500 offset ||= 0
501 501
502 502 [offset, limit]
503 503 end
504 504
505 505 # qvalues http header parser
506 506 # code taken from webrick
507 507 def parse_qvalues(value)
508 508 tmp = []
509 509 if value
510 510 parts = value.split(/,\s*/)
511 511 parts.each {|part|
512 512 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
513 513 val = m[1]
514 514 q = (m[2] or 1).to_f
515 515 tmp.push([val, q])
516 516 end
517 517 }
518 518 tmp = tmp.sort_by{|val, q| -q}
519 519 tmp.collect!{|val, q| val}
520 520 end
521 521 return tmp
522 522 rescue
523 523 nil
524 524 end
525 525
526 526 # Returns a string that can be used as filename value in Content-Disposition header
527 527 def filename_for_content_disposition(name)
528 528 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
529 529 end
530 530
531 531 def api_request?
532 532 %w(xml json).include? params[:format]
533 533 end
534 534
535 535 # Returns the API key present in the request
536 536 def api_key_from_request
537 537 if params[:key].present?
538 538 params[:key].to_s
539 539 elsif request.headers["X-Redmine-API-Key"].present?
540 540 request.headers["X-Redmine-API-Key"].to_s
541 541 end
542 542 end
543 543
544 544 # Returns the API 'switch user' value if present
545 545 def api_switch_user_from_request
546 546 request.headers["X-Redmine-Switch-User"].to_s.presence
547 547 end
548 548
549 549 # Renders a warning flash if obj has unsaved attachments
550 550 def render_attachment_warning_if_needed(obj)
551 551 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
552 552 end
553 553
554 554 # Sets the `flash` notice or error based the number of issues that did not save
555 555 #
556 556 # @param [Array, Issue] issues all of the saved and unsaved Issues
557 557 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
558 558 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
559 559 if unsaved_issue_ids.empty?
560 560 flash[:notice] = l(:notice_successful_update) unless issues.empty?
561 561 else
562 562 flash[:error] = l(:notice_failed_to_save_issues,
563 563 :count => unsaved_issue_ids.size,
564 564 :total => issues.size,
565 565 :ids => '#' + unsaved_issue_ids.join(', #'))
566 566 end
567 567 end
568 568
569 569 # Rescues an invalid query statement. Just in case...
570 570 def query_statement_invalid(exception)
571 571 logger.error "Query::StatementInvalid: #{exception.message}" if logger
572 572 session.delete(:query)
573 573 sort_clear if respond_to?(:sort_clear)
574 574 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
575 575 end
576 576
577 577 # Renders a 200 response for successfull updates or deletions via the API
578 578 def render_api_ok
579 579 render_api_head :ok
580 580 end
581 581
582 582 # Renders a head API response
583 583 def render_api_head(status)
584 584 # #head would return a response body with one space
585 585 render :text => '', :status => status, :layout => nil
586 586 end
587 587
588 588 # Renders API response on validation failure
589 589 def render_validation_errors(objects)
590 590 if objects.is_a?(Array)
591 591 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
592 592 else
593 593 @error_messages = objects.errors.full_messages
594 594 end
595 595 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
596 596 end
597 597
598 598 # Overrides #_include_layout? so that #render with no arguments
599 599 # doesn't use the layout for api requests
600 600 def _include_layout?(*args)
601 601 api_request? ? false : super
602 602 end
603 603 end
@@ -1,154 +1,154
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AttachmentsController < ApplicationController
19 19 before_filter :find_project, :except => :upload
20 20 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 21 before_filter :delete_authorize, :only => :destroy
22 22 before_filter :authorize_global, :only => :upload
23 23
24 24 accept_api_auth :show, :download, :upload
25 25
26 26 def show
27 27 respond_to do |format|
28 28 format.html {
29 29 if @attachment.is_diff?
30 30 @diff = File.new(@attachment.diskfile, "rb").read
31 31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 33 # Save diff type as user preference
34 34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 35 User.current.pref[:diff_type] = @diff_type
36 36 User.current.preference.save
37 37 end
38 38 render :action => 'diff'
39 39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 40 @content = File.new(@attachment.diskfile, "rb").read
41 41 render :action => 'file'
42 42 else
43 43 download
44 44 end
45 45 }
46 46 format.api
47 47 end
48 48 end
49 49
50 50 def download
51 51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
52 52 @attachment.increment_download
53 53 end
54 54
55 55 if stale?(:etag => @attachment.digest)
56 56 # images are sent inline
57 57 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
58 58 :type => detect_content_type(@attachment),
59 59 :disposition => (@attachment.image? ? 'inline' : 'attachment')
60 60 end
61 61 end
62 62
63 63 def thumbnail
64 64 if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
65 65 if stale?(:etag => thumbnail)
66 66 send_file thumbnail,
67 67 :filename => filename_for_content_disposition(@attachment.filename),
68 68 :type => detect_content_type(@attachment),
69 69 :disposition => 'inline'
70 70 end
71 71 else
72 72 # No thumbnail for the attachment or thumbnail could not be created
73 73 render :nothing => true, :status => 404
74 74 end
75 75 end
76 76
77 77 def upload
78 78 # Make sure that API users get used to set this content type
79 79 # as it won't trigger Rails' automatic parsing of the request body for parameters
80 80 unless request.content_type == 'application/octet-stream'
81 81 render :nothing => true, :status => 406
82 82 return
83 83 end
84 84
85 85 @attachment = Attachment.new(:file => request.raw_post)
86 86 @attachment.author = User.current
87 87 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
88 88 saved = @attachment.save
89 89
90 90 respond_to do |format|
91 91 format.js
92 92 format.api {
93 93 if saved
94 94 render :action => 'upload', :status => :created
95 95 else
96 96 render_validation_errors(@attachment)
97 97 end
98 98 }
99 99 end
100 100 end
101 101
102 102 def destroy
103 103 if @attachment.container.respond_to?(:init_journal)
104 104 @attachment.container.init_journal(User.current)
105 105 end
106 106 if @attachment.container
107 107 # Make sure association callbacks are called
108 108 @attachment.container.attachments.delete(@attachment)
109 109 else
110 110 @attachment.destroy
111 111 end
112 112
113 113 respond_to do |format|
114 114 format.html { redirect_to_referer_or project_path(@project) }
115 115 format.js
116 116 end
117 117 end
118 118
119 119 private
120 120 def find_project
121 121 @attachment = Attachment.find(params[:id])
122 122 # Show 404 if the filename in the url is wrong
123 123 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
124 124 @project = @attachment.project
125 125 rescue ActiveRecord::RecordNotFound
126 126 render_404
127 127 end
128 128
129 129 # Checks that the file exists and is readable
130 130 def file_readable
131 131 if @attachment.readable?
132 132 true
133 133 else
134 134 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
135 135 render_404
136 136 end
137 137 end
138 138
139 139 def read_authorize
140 140 @attachment.visible? ? true : deny_access
141 141 end
142 142
143 143 def delete_authorize
144 144 @attachment.deletable? ? true : deny_access
145 145 end
146 146
147 147 def detect_content_type(attachment)
148 148 content_type = attachment.content_type
149 149 if content_type.blank?
150 150 content_type = Redmine::MimeType.of(attachment.filename)
151 151 end
152 152 content_type.to_s
153 153 end
154 154 end
@@ -1,96 +1,96
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AutoCompletesController < ApplicationController
19 19 before_filter :find_project
20 20
21 21 def issues
22 22 @issues = []
23 23 q = (params[:q] || params[:term]).to_s.strip
24 24 if q.present?
25 25 scope = (params[:scope] == "all" || @project.nil? ? Issue : @project.issues).visible
26 26 if q.match(/^\d+$/)
27 27 @issues << scope.find_by_id(q.to_i)
28 28 end
29 29 @issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%").order("#{Issue.table_name}.id DESC").limit(10).all
30 30 @issues.compact!
31 31 end
32 32 render :layout => false
33 33 end
34 34
35 35 private
36 36
37 37 def find_project
38 38 if params[:project_id].present?
39 39 @project = Project.find(params[:project_id])
40 40 end
41 41 rescue ActiveRecord::RecordNotFound
42 42 render_404
43 43 end
44 44 end
@@ -1,110 +1,110
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class BoardsController < ApplicationController
19 19 default_search_scope :messages
20 20 before_filter :find_project_by_project_id, :find_board_if_available, :authorize
21 21 accept_rss_auth :index, :show
22 22
23 23 helper :sort
24 24 include SortHelper
25 25 helper :watchers
26 26
27 27 def index
28 28 @boards = @project.boards.includes(:last_message => :author).all
29 29 # show the board if there is only one
30 30 if @boards.size == 1
31 31 @board = @boards.first
32 32 show
33 33 end
34 34 end
35 35
36 36 def show
37 37 respond_to do |format|
38 38 format.html {
39 39 sort_init 'updated_on', 'desc'
40 40 sort_update 'created_on' => "#{Message.table_name}.created_on",
41 41 'replies' => "#{Message.table_name}.replies_count",
42 42 'updated_on' => "#{Message.table_name}.updated_on"
43 43
44 44 @topic_count = @board.topics.count
45 45 @topic_pages = Paginator.new @topic_count, per_page_option, params['page']
46 46 @topics = @board.topics.
47 47 reorder("#{Message.table_name}.sticky DESC").
48 48 includes(:author, {:last_reply => :author}).
49 49 limit(@topic_pages.items_per_page).
50 50 offset(@topic_pages.offset).
51 51 order(sort_clause).
52 52 all
53 53 @message = Message.new(:board => @board)
54 54 render :action => 'show', :layout => !request.xhr?
55 55 }
56 56 format.atom {
57 57 @messages = @board.messages.
58 58 reorder('created_on DESC').
59 59 includes(:author, :board).
60 60 limit(Setting.feeds_limit.to_i).
61 61 all
62 62 render_feed(@messages, :title => "#{@project}: #{@board}")
63 63 }
64 64 end
65 65 end
66 66
67 67 def new
68 68 @board = @project.boards.build
69 69 @board.safe_attributes = params[:board]
70 70 end
71 71
72 72 def create
73 73 @board = @project.boards.build
74 74 @board.safe_attributes = params[:board]
75 75 if @board.save
76 76 flash[:notice] = l(:notice_successful_create)
77 77 redirect_to_settings_in_projects
78 78 else
79 79 render :action => 'new'
80 80 end
81 81 end
82 82
83 83 def edit
84 84 end
85 85
86 86 def update
87 87 @board.safe_attributes = params[:board]
88 88 if @board.save
89 89 redirect_to_settings_in_projects
90 90 else
91 91 render :action => 'edit'
92 92 end
93 93 end
94 94
95 95 def destroy
96 96 @board.destroy
97 97 redirect_to_settings_in_projects
98 98 end
99 99
100 100 private
101 101 def redirect_to_settings_in_projects
102 102 redirect_to settings_project_path(@project, :tab => 'boards')
103 103 end
104 104
105 105 def find_board_if_available
106 106 @board = @project.boards.find(params[:id]) if params[:id]
107 107 rescue ActiveRecord::RecordNotFound
108 108 render_404
109 109 end
110 110 end
@@ -1,56 +1,56
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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,86 +1,86
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 def issues
23 23 @issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
24 24 if (@issues.size == 1)
25 25 @issue = @issues.first
26 26 end
27 27 @issue_ids = @issues.map(&:id).sort
28 28
29 29 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
30 30 @projects = @issues.collect(&:project).compact.uniq
31 31 @project = @projects.first if @projects.size == 1
32 32
33 33 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
34 34 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
35 35 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
36 36 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
37 37 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
38 38 :delete => User.current.allowed_to?(:delete_issues, @projects)
39 39 }
40 40 if @project
41 41 if @issue
42 42 @assignables = @issue.assignable_users
43 43 else
44 44 @assignables = @project.assignable_users
45 45 end
46 46 @trackers = @project.trackers
47 47 else
48 48 #when multiple projects, we only keep the intersection of each set
49 49 @assignables = @projects.map(&:assignable_users).reduce(:&)
50 50 @trackers = @projects.map(&:trackers).reduce(:&)
51 51 end
52 52 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
53 53
54 54 @priorities = IssuePriority.active.reverse
55 55 @back = back_url
56 56
57 57 @options_by_custom_field = {}
58 58 if @can[:edit]
59 59 custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f|
60 60 %w(bool list user version).include?(f.field_format) && !f.multiple?
61 61 end
62 62 custom_fields.each do |field|
63 63 values = field.possible_values_options(@projects)
64 64 if values.any?
65 65 @options_by_custom_field[field] = values
66 66 end
67 67 end
68 68 end
69 69
70 70 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
71 71 render :layout => false
72 72 end
73 73
74 74 def time_entries
75 75 @time_entries = TimeEntry.all(
76 76 :conditions => {:id => params[:ids]}, :include => :project)
77 77 @projects = @time_entries.collect(&:project).compact.uniq
78 78 @project = @projects.first if @projects.size == 1
79 79 @activities = TimeEntryActivity.shared.active
80 80 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
81 81 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
82 82 }
83 83 @back = back_url
84 84 render :layout => false
85 85 end
86 86 end
@@ -1,79 +1,79
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
25 25 def index
26 26 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
27 27 @tab = params[:tab] || 'IssueCustomField'
28 28 end
29 29
30 30 def new
31 31 end
32 32
33 33 def create
34 34 if @custom_field.save
35 35 flash[:notice] = l(:notice_successful_create)
36 36 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
37 37 redirect_to custom_fields_path(:tab => @custom_field.class.name)
38 38 else
39 39 render :action => 'new'
40 40 end
41 41 end
42 42
43 43 def edit
44 44 end
45 45
46 46 def update
47 47 if @custom_field.update_attributes(params[:custom_field])
48 48 flash[:notice] = l(:notice_successful_update)
49 49 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
50 50 redirect_to custom_fields_path(:tab => @custom_field.class.name)
51 51 else
52 52 render :action => 'edit'
53 53 end
54 54 end
55 55
56 56 def destroy
57 57 begin
58 58 @custom_field.destroy
59 59 rescue
60 60 flash[:error] = l(:error_can_not_delete_custom_field)
61 61 end
62 62 redirect_to custom_fields_path(:tab => @custom_field.class.name)
63 63 end
64 64
65 65 private
66 66
67 67 def build_new_custom_field
68 68 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
69 69 if @custom_field.nil?
70 70 render_404
71 71 end
72 72 end
73 73
74 74 def find_custom_field
75 75 @custom_field = CustomField.find(params[:id])
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79 end
@@ -1,94 +1,94
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class DocumentsController < ApplicationController
19 19 default_search_scope :documents
20 20 model_object Document
21 21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
22 22 before_filter :find_model_object, :except => [:index, :new, :create]
23 23 before_filter :find_project_from_association, :except => [:index, :new, :create]
24 24 before_filter :authorize
25 25
26 26 helper :attachments
27 27
28 28 def index
29 29 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
30 30 documents = @project.documents.includes(:attachments, :category).all
31 31 case @sort_by
32 32 when 'date'
33 33 @grouped = documents.group_by {|d| d.updated_on.to_date }
34 34 when 'title'
35 35 @grouped = documents.group_by {|d| d.title.first.upcase}
36 36 when 'author'
37 37 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
38 38 else
39 39 @grouped = documents.group_by(&:category)
40 40 end
41 41 @document = @project.documents.build
42 42 render :layout => false if request.xhr?
43 43 end
44 44
45 45 def show
46 46 @attachments = @document.attachments.all
47 47 end
48 48
49 49 def new
50 50 @document = @project.documents.build
51 51 @document.safe_attributes = params[:document]
52 52 end
53 53
54 54 def create
55 55 @document = @project.documents.build
56 56 @document.safe_attributes = params[:document]
57 57 @document.save_attachments(params[:attachments])
58 58 if @document.save
59 59 render_attachment_warning_if_needed(@document)
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to project_documents_path(@project)
62 62 else
63 63 render :action => 'new'
64 64 end
65 65 end
66 66
67 67 def edit
68 68 end
69 69
70 70 def update
71 71 @document.safe_attributes = params[:document]
72 72 if request.put? and @document.save
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to document_path(@document)
75 75 else
76 76 render :action => 'edit'
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @document.destroy if request.delete?
82 82 redirect_to project_documents_path(@project)
83 83 end
84 84
85 85 def add_attachment
86 86 attachments = Attachment.attach_files(@document, params[:attachments])
87 87 render_attachment_warning_if_needed(@document)
88 88
89 89 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
90 90 Mailer.attachments_added(attachments[:files]).deliver
91 91 end
92 92 redirect_to document_path(@document)
93 93 end
94 94 end
@@ -1,98 +1,98
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EnumerationsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 before_filter :build_new_enumeration, :only => [:new, :create]
24 24 before_filter :find_enumeration, :only => [:edit, :update, :destroy]
25 25 accept_api_auth :index
26 26
27 27 helper :custom_fields
28 28
29 29 def index
30 30 respond_to do |format|
31 31 format.html
32 32 format.api {
33 33 @klass = Enumeration.get_subclass(params[:type])
34 34 if @klass
35 35 @enumerations = @klass.shared.sorted.all
36 36 else
37 37 render_404
38 38 end
39 39 }
40 40 end
41 41 end
42 42
43 43 def new
44 44 end
45 45
46 46 def create
47 47 if request.post? && @enumeration.save
48 48 flash[:notice] = l(:notice_successful_create)
49 49 redirect_to enumerations_path
50 50 else
51 51 render :action => 'new'
52 52 end
53 53 end
54 54
55 55 def edit
56 56 end
57 57
58 58 def update
59 59 if request.put? && @enumeration.update_attributes(params[:enumeration])
60 60 flash[:notice] = l(:notice_successful_update)
61 61 redirect_to enumerations_path
62 62 else
63 63 render :action => 'edit'
64 64 end
65 65 end
66 66
67 67 def destroy
68 68 if !@enumeration.in_use?
69 69 # No associated objects
70 70 @enumeration.destroy
71 71 redirect_to enumerations_path
72 72 return
73 73 elsif params[:reassign_to_id]
74 74 if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
75 75 @enumeration.destroy(reassign_to)
76 76 redirect_to enumerations_path
77 77 return
78 78 end
79 79 end
80 80 @enumerations = @enumeration.class.all - [@enumeration]
81 81 end
82 82
83 83 private
84 84
85 85 def build_new_enumeration
86 86 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
87 87 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
88 88 if @enumeration.nil?
89 89 render_404
90 90 end
91 91 end
92 92
93 93 def find_enumeration
94 94 @enumeration = Enumeration.find(params[:id])
95 95 rescue ActiveRecord::RecordNotFound
96 96 render_404
97 97 end
98 98 end
@@ -1,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class FilesController < ApplicationController
19 19 menu_item :files
20 20
21 21 before_filter :find_project_by_project_id
22 22 before_filter :authorize
23 23
24 24 helper :sort
25 25 include SortHelper
26 26
27 27 def index
28 28 sort_init 'filename', 'asc'
29 29 sort_update 'filename' => "#{Attachment.table_name}.filename",
30 30 'created_on' => "#{Attachment.table_name}.created_on",
31 31 'size' => "#{Attachment.table_name}.filesize",
32 32 'downloads' => "#{Attachment.table_name}.downloads"
33 33
34 34 @containers = [ Project.includes(:attachments).reorder(sort_clause).find(@project.id)]
35 35 @containers += @project.versions.includes(:attachments).reorder(sort_clause).all.sort.reverse
36 36 render :layout => !request.xhr?
37 37 end
38 38
39 39 def new
40 40 @versions = @project.versions.sort
41 41 end
42 42
43 43 def create
44 44 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
45 45 attachments = Attachment.attach_files(container, params[:attachments])
46 46 render_attachment_warning_if_needed(container)
47 47
48 48 if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
49 49 Mailer.attachments_added(attachments[:files]).deliver
50 50 end
51 51 redirect_to project_files_path(@project)
52 52 end
53 53 end
@@ -1,48 +1,48
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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,140 +1,140
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class GroupsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_group, :except => [:index, :new, :create]
23 23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24 24
25 25 helper :custom_fields
26 26
27 27 def index
28 28 @groups = Group.sorted.all
29 29
30 30 respond_to do |format|
31 31 format.html
32 32 format.api
33 33 end
34 34 end
35 35
36 36 def show
37 37 respond_to do |format|
38 38 format.html
39 39 format.api
40 40 end
41 41 end
42 42
43 43 def new
44 44 @group = Group.new
45 45 end
46 46
47 47 def create
48 48 @group = Group.new
49 49 @group.safe_attributes = params[:group]
50 50
51 51 respond_to do |format|
52 52 if @group.save
53 53 format.html {
54 54 flash[:notice] = l(:notice_successful_create)
55 55 redirect_to(params[:continue] ? new_group_path : groups_path)
56 56 }
57 57 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
58 58 else
59 59 format.html { render :action => "new" }
60 60 format.api { render_validation_errors(@group) }
61 61 end
62 62 end
63 63 end
64 64
65 65 def edit
66 66 end
67 67
68 68 def update
69 69 @group.safe_attributes = params[:group]
70 70
71 71 respond_to do |format|
72 72 if @group.save
73 73 flash[:notice] = l(:notice_successful_update)
74 74 format.html { redirect_to(groups_path) }
75 75 format.api { render_api_ok }
76 76 else
77 77 format.html { render :action => "edit" }
78 78 format.api { render_validation_errors(@group) }
79 79 end
80 80 end
81 81 end
82 82
83 83 def destroy
84 84 @group.destroy
85 85
86 86 respond_to do |format|
87 87 format.html { redirect_to(groups_path) }
88 88 format.api { render_api_ok }
89 89 end
90 90 end
91 91
92 92 def add_users
93 93 @users = User.find_all_by_id(params[:user_id] || params[:user_ids])
94 94 @group.users << @users if request.post?
95 95 respond_to do |format|
96 96 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
97 97 format.js
98 98 format.api { render_api_ok }
99 99 end
100 100 end
101 101
102 102 def remove_user
103 103 @group.users.delete(User.find(params[:user_id])) if request.delete?
104 104 respond_to do |format|
105 105 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
106 106 format.js
107 107 format.api { render_api_ok }
108 108 end
109 109 end
110 110
111 111 def autocomplete_for_user
112 112 @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
113 113 render :layout => false
114 114 end
115 115
116 116 def edit_membership
117 117 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
118 118 @membership.save if request.post?
119 119 respond_to do |format|
120 120 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
121 121 format.js
122 122 end
123 123 end
124 124
125 125 def destroy_membership
126 126 Member.find(params[:membership_id]).destroy if request.post?
127 127 respond_to do |format|
128 128 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
129 129 format.js
130 130 end
131 131 end
132 132
133 133 private
134 134
135 135 def find_group
136 136 @group = Group.find(params[:id])
137 137 rescue ActiveRecord::RecordNotFound
138 138 render_404
139 139 end
140 140 end
@@ -1,122 +1,122
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueCategoriesController < ApplicationController
19 19 menu_item :settings
20 20 model_object IssueCategory
21 21 before_filter :find_model_object, :except => [:index, :new, :create]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
24 24 before_filter :authorize
25 25 accept_api_auth :index, :show, :create, :update, :destroy
26 26
27 27 def index
28 28 respond_to do |format|
29 29 format.html { redirect_to_settings_in_projects }
30 30 format.api { @categories = @project.issue_categories.all }
31 31 end
32 32 end
33 33
34 34 def show
35 35 respond_to do |format|
36 36 format.html { redirect_to_settings_in_projects }
37 37 format.api
38 38 end
39 39 end
40 40
41 41 def new
42 42 @category = @project.issue_categories.build
43 43 @category.safe_attributes = params[:issue_category]
44 44
45 45 respond_to do |format|
46 46 format.html
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def create
52 52 @category = @project.issue_categories.build
53 53 @category.safe_attributes = params[:issue_category]
54 54 if @category.save
55 55 respond_to do |format|
56 56 format.html do
57 57 flash[:notice] = l(:notice_successful_create)
58 58 redirect_to_settings_in_projects
59 59 end
60 60 format.js
61 61 format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
62 62 end
63 63 else
64 64 respond_to do |format|
65 65 format.html { render :action => 'new'}
66 66 format.js { render :action => 'new'}
67 67 format.api { render_validation_errors(@category) }
68 68 end
69 69 end
70 70 end
71 71
72 72 def edit
73 73 end
74 74
75 75 def update
76 76 @category.safe_attributes = params[:issue_category]
77 77 if @category.save
78 78 respond_to do |format|
79 79 format.html {
80 80 flash[:notice] = l(:notice_successful_update)
81 81 redirect_to_settings_in_projects
82 82 }
83 83 format.api { render_api_ok }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 format.html { render :action => 'edit' }
88 88 format.api { render_validation_errors(@category) }
89 89 end
90 90 end
91 91 end
92 92
93 93 def destroy
94 94 @issue_count = @category.issues.size
95 95 if @issue_count == 0 || params[:todo] || api_request?
96 96 reassign_to = nil
97 97 if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
98 98 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
99 99 end
100 100 @category.destroy(reassign_to)
101 101 respond_to do |format|
102 102 format.html { redirect_to_settings_in_projects }
103 103 format.api { render_api_ok }
104 104 end
105 105 return
106 106 end
107 107 @categories = @project.issue_categories - [@category]
108 108 end
109 109
110 110 private
111 111
112 112 def redirect_to_settings_in_projects
113 113 redirect_to settings_project_path(@project, :tab => 'categories')
114 114 end
115 115
116 116 # Wrap ApplicationController's find_model_object method to set
117 117 # @category instead of just @issue_category
118 118 def find_model_object
119 119 super
120 120 @category = @object
121 121 end
122 122 end
@@ -1,88 +1,88
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
20 20 before_filter :find_relation, :except => [:index, :create]
21 21
22 22 accept_api_auth :index, :show, :create, :destroy
23 23
24 24 def index
25 25 @relations = @issue.relations
26 26
27 27 respond_to do |format|
28 28 format.html { render :nothing => true }
29 29 format.api
30 30 end
31 31 end
32 32
33 33 def show
34 34 raise Unauthorized unless @relation.visible?
35 35
36 36 respond_to do |format|
37 37 format.html { render :nothing => true }
38 38 format.api
39 39 end
40 40 end
41 41
42 42 def create
43 43 @relation = IssueRelation.new(params[:relation])
44 44 @relation.issue_from = @issue
45 45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
46 46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
47 47 end
48 48 saved = @relation.save
49 49
50 50 respond_to do |format|
51 51 format.html { redirect_to issue_path(@issue) }
52 52 format.js {
53 53 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
54 54 }
55 55 format.api {
56 56 if saved
57 57 render :action => 'show', :status => :created, :location => relation_url(@relation)
58 58 else
59 59 render_validation_errors(@relation)
60 60 end
61 61 }
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 raise Unauthorized unless @relation.deletable?
67 67 @relation.destroy
68 68
69 69 respond_to do |format|
70 70 format.html { redirect_to issue_path(@relation.issue_from) }
71 71 format.js
72 72 format.api { render_api_ok }
73 73 end
74 74 end
75 75
76 76 private
77 77 def find_issue
78 78 @issue = @object = Issue.find(params[:issue_id])
79 79 rescue ActiveRecord::RecordNotFound
80 80 render_404
81 81 end
82 82
83 83 def find_relation
84 84 @relation = IssueRelation.find(params[:id])
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88 end
@@ -1,81 +1,81
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.all(:order => 'position')
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 request.post? && @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 request.put? && @issue_status.update_attributes(params[:issue_status])
58 58 flash[:notice] = l(:notice_successful_update)
59 59 redirect_to issue_statuses_path
60 60 else
61 61 render :action => 'edit'
62 62 end
63 63 end
64 64
65 65 def destroy
66 66 IssueStatus.find(params[:id]).destroy
67 67 redirect_to issue_statuses_path
68 68 rescue
69 69 flash[:error] = l(:error_unable_delete_issue_status)
70 70 redirect_to issue_statuses_path
71 71 end
72 72
73 73 def update_issue_done_ratio
74 74 if request.post? && IssueStatus.update_issue_done_ratios
75 75 flash[:notice] = l(:notice_issue_done_ratios_updated)
76 76 else
77 77 flash[:error] = l(:error_issue_done_ratios_not_updated)
78 78 end
79 79 redirect_to issue_statuses_path
80 80 end
81 81 end
@@ -1,435 +1,435
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 29 accept_rss_auth :index, :show
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 helper :sort
50 50 include SortHelper
51 51 include IssuesHelper
52 52 helper :timelog
53 53 include Redmine::Export::PDF
54 54
55 55 def index
56 56 retrieve_query
57 57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 58 sort_update(@query.sortable_columns)
59 59 @query.sort_criteria = sort_criteria.to_a
60 60
61 61 if @query.valid?
62 62 case params[:format]
63 63 when 'csv', 'pdf'
64 64 @limit = Setting.issues_export_limit.to_i
65 65 when 'atom'
66 66 @limit = Setting.feeds_limit.to_i
67 67 when 'xml', 'json'
68 68 @offset, @limit = api_offset_and_limit
69 69 else
70 70 @limit = per_page_option
71 71 end
72 72
73 73 @issue_count = @query.issue_count
74 74 @issue_pages = Paginator.new @issue_count, @limit, params['page']
75 75 @offset ||= @issue_pages.offset
76 76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
77 77 :order => sort_clause,
78 78 :offset => @offset,
79 79 :limit => @limit)
80 80 @issue_count_by_group = @query.issue_count_by_group
81 81
82 82 respond_to do |format|
83 83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
84 84 format.api {
85 85 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
86 86 }
87 87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
88 88 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
89 89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
90 90 end
91 91 else
92 92 respond_to do |format|
93 93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
94 94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
95 95 format.api { render_validation_errors(@query) }
96 96 end
97 97 end
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101
102 102 def show
103 103 @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
104 104 @journals.each_with_index {|j,i| j.indice = i+1}
105 105 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
106 106 @journals.reverse! if User.current.wants_comments_in_reverse_order?
107 107
108 108 @changesets = @issue.changesets.visible.all
109 109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 110
111 111 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
112 112 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 113 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 114 @priorities = IssuePriority.active
115 115 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
116 116 respond_to do |format|
117 117 format.html {
118 118 retrieve_previous_and_next_issue_ids
119 119 render :template => 'issues/show'
120 120 }
121 121 format.api
122 122 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
123 123 format.pdf {
124 124 pdf = issue_to_pdf(@issue, :journals => @journals)
125 125 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
126 126 }
127 127 end
128 128 end
129 129
130 130 # Add a new issue
131 131 # The new issue will be created from an existing one if copy_from parameter is given
132 132 def new
133 133 respond_to do |format|
134 134 format.html { render :action => 'new', :layout => !request.xhr? }
135 135 format.js { render :partial => 'update_form' }
136 136 end
137 137 end
138 138
139 139 def create
140 140 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
141 141 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
142 142 if @issue.save
143 143 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
144 144 respond_to do |format|
145 145 format.html {
146 146 render_attachment_warning_if_needed(@issue)
147 147 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
148 148 if params[:continue]
149 149 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
150 150 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
151 151 else
152 152 redirect_to issue_path(@issue)
153 153 end
154 154 }
155 155 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
156 156 end
157 157 return
158 158 else
159 159 respond_to do |format|
160 160 format.html { render :action => 'new' }
161 161 format.api { render_validation_errors(@issue) }
162 162 end
163 163 end
164 164 end
165 165
166 166 def edit
167 167 return unless update_issue_from_params
168 168
169 169 respond_to do |format|
170 170 format.html { }
171 171 format.xml { }
172 172 end
173 173 end
174 174
175 175 def update
176 176 return unless update_issue_from_params
177 177 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 178 saved = false
179 179 begin
180 180 saved = @issue.save_issue_with_child_records(params, @time_entry)
181 181 rescue ActiveRecord::StaleObjectError
182 182 @conflict = true
183 183 if params[:last_journal_id]
184 184 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
185 185 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
186 186 end
187 187 end
188 188
189 189 if saved
190 190 render_attachment_warning_if_needed(@issue)
191 191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 192
193 193 respond_to do |format|
194 194 format.html { redirect_back_or_default issue_path(@issue) }
195 195 format.api { render_api_ok }
196 196 end
197 197 else
198 198 respond_to do |format|
199 199 format.html { render :action => 'edit' }
200 200 format.api { render_validation_errors(@issue) }
201 201 end
202 202 end
203 203 end
204 204
205 205 # Bulk edit/copy a set of issues
206 206 def bulk_edit
207 207 @issues.sort!
208 208 @copy = params[:copy].present?
209 209 @notes = params[:notes]
210 210
211 211 if User.current.allowed_to?(:move_issues, @projects)
212 212 @allowed_projects = Issue.allowed_target_projects_on_move
213 213 if params[:issue]
214 214 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
215 215 if @target_project
216 216 target_projects = [@target_project]
217 217 end
218 218 end
219 219 end
220 220 target_projects ||= @projects
221 221
222 222 if @copy
223 223 @available_statuses = [IssueStatus.default]
224 224 else
225 225 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
226 226 end
227 227 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
228 228 @assignables = target_projects.map(&:assignable_users).reduce(:&)
229 229 @trackers = target_projects.map(&:trackers).reduce(:&)
230 230 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
231 231 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
232 232 if @copy
233 233 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
234 234 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
235 235 end
236 236
237 237 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
238 238 render :layout => false if request.xhr?
239 239 end
240 240
241 241 def bulk_update
242 242 @issues.sort!
243 243 @copy = params[:copy].present?
244 244 attributes = parse_params_for_bulk_issue_attributes(params)
245 245
246 246 unsaved_issue_ids = []
247 247 moved_issues = []
248 248
249 249 if @copy && params[:copy_subtasks].present?
250 250 # Descendant issues will be copied with the parent task
251 251 # Don't copy them twice
252 252 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
253 253 end
254 254
255 255 @issues.each do |issue|
256 256 issue.reload
257 257 if @copy
258 258 issue = issue.copy({},
259 259 :attachments => params[:copy_attachments].present?,
260 260 :subtasks => params[:copy_subtasks].present?
261 261 )
262 262 end
263 263 journal = issue.init_journal(User.current, params[:notes])
264 264 issue.safe_attributes = attributes
265 265 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
266 266 if issue.save
267 267 moved_issues << issue
268 268 else
269 269 # Keep unsaved issue ids to display them in flash error
270 270 unsaved_issue_ids << issue.id
271 271 end
272 272 end
273 273 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
274 274
275 275 if params[:follow]
276 276 if @issues.size == 1 && moved_issues.size == 1
277 277 redirect_to issue_path(moved_issues.first)
278 278 elsif moved_issues.map(&:project).uniq.size == 1
279 279 redirect_to project_issues_path(moved_issues.map(&:project).first)
280 280 end
281 281 else
282 282 redirect_back_or_default _project_issues_path(@project)
283 283 end
284 284 end
285 285
286 286 def destroy
287 287 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
288 288 if @hours > 0
289 289 case params[:todo]
290 290 when 'destroy'
291 291 # nothing to do
292 292 when 'nullify'
293 293 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
294 294 when 'reassign'
295 295 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
296 296 if reassign_to.nil?
297 297 flash.now[:error] = l(:error_issue_not_found_in_project)
298 298 return
299 299 else
300 300 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
301 301 end
302 302 else
303 303 # display the destroy form if it's a user request
304 304 return unless api_request?
305 305 end
306 306 end
307 307 @issues.each do |issue|
308 308 begin
309 309 issue.reload.destroy
310 310 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
311 311 # nothing to do, issue was already deleted (eg. by a parent)
312 312 end
313 313 end
314 314 respond_to do |format|
315 315 format.html { redirect_back_or_default _project_issues_path(@project) }
316 316 format.api { render_api_ok }
317 317 end
318 318 end
319 319
320 320 private
321 321
322 322 def find_project
323 323 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
324 324 @project = Project.find(project_id)
325 325 rescue ActiveRecord::RecordNotFound
326 326 render_404
327 327 end
328 328
329 329 def retrieve_previous_and_next_issue_ids
330 330 retrieve_query_from_session
331 331 if @query
332 332 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
333 333 sort_update(@query.sortable_columns, 'issues_index_sort')
334 334 limit = 500
335 335 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
336 336 if (idx = issue_ids.index(@issue.id)) && idx < limit
337 337 if issue_ids.size < 500
338 338 @issue_position = idx + 1
339 339 @issue_count = issue_ids.size
340 340 end
341 341 @prev_issue_id = issue_ids[idx - 1] if idx > 0
342 342 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
343 343 end
344 344 end
345 345 end
346 346
347 347 # Used by #edit and #update to set some common instance variables
348 348 # from the params
349 349 # TODO: Refactor, not everything in here is needed by #edit
350 350 def update_issue_from_params
351 351 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
352 352 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
353 353 @time_entry.attributes = params[:time_entry]
354 354
355 355 @issue.init_journal(User.current)
356 356
357 357 issue_attributes = params[:issue]
358 358 if issue_attributes && params[:conflict_resolution]
359 359 case params[:conflict_resolution]
360 360 when 'overwrite'
361 361 issue_attributes = issue_attributes.dup
362 362 issue_attributes.delete(:lock_version)
363 363 when 'add_notes'
364 364 issue_attributes = issue_attributes.slice(:notes)
365 365 when 'cancel'
366 366 redirect_to issue_path(@issue)
367 367 return false
368 368 end
369 369 end
370 370 @issue.safe_attributes = issue_attributes
371 371 @priorities = IssuePriority.active
372 372 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
373 373 true
374 374 end
375 375
376 376 # TODO: Refactor, lots of extra code in here
377 377 # TODO: Changing tracker on an existing issue should not trigger this
378 378 def build_new_issue_from_params
379 379 if params[:id].blank?
380 380 @issue = Issue.new
381 381 if params[:copy_from]
382 382 begin
383 383 @copy_from = Issue.visible.find(params[:copy_from])
384 384 @copy_attachments = params[:copy_attachments].present? || request.get?
385 385 @copy_subtasks = params[:copy_subtasks].present? || request.get?
386 386 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
387 387 rescue ActiveRecord::RecordNotFound
388 388 render_404
389 389 return
390 390 end
391 391 end
392 392 @issue.project = @project
393 393 else
394 394 @issue = @project.issues.visible.find(params[:id])
395 395 end
396 396
397 397 @issue.project = @project
398 398 @issue.author ||= User.current
399 399 # Tracker must be set before custom field values
400 400 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
401 401 if @issue.tracker.nil?
402 402 render_error l(:error_no_tracker_in_project)
403 403 return false
404 404 end
405 405 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
406 406 @issue.safe_attributes = params[:issue]
407 407
408 408 @priorities = IssuePriority.active
409 409 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
410 410 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
411 411 end
412 412
413 413 def check_for_default_issue_status
414 414 if IssueStatus.default.nil?
415 415 render_error l(:error_no_default_issue_status)
416 416 return false
417 417 end
418 418 end
419 419
420 420 def parse_params_for_bulk_issue_attributes(params)
421 421 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
422 422 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
423 423 if custom = attributes[:custom_field_values]
424 424 custom.reject! {|k,v| v.blank?}
425 425 custom.keys.each do |k|
426 426 if custom[k].is_a?(Array)
427 427 custom[k] << '' if custom[k].delete('__none__')
428 428 else
429 429 custom[k] = '' if custom[k] == '__none__'
430 430 end
431 431 end
432 432 end
433 433 attributes
434 434 end
435 435 end
@@ -1,105 +1,105
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class JournalsController < ApplicationController
19 19 before_filter :find_journal, :only => [:edit, :diff]
20 20 before_filter :find_issue, :only => [:new]
21 21 before_filter :find_optional_project, :only => [:index]
22 22 before_filter :authorize, :only => [:new, :edit, :diff]
23 23 accept_rss_auth :index
24 24 menu_item :issues
25 25
26 26 helper :issues
27 27 helper :custom_fields
28 28 helper :queries
29 29 include QueriesHelper
30 30 helper :sort
31 31 include SortHelper
32 32
33 33 def index
34 34 retrieve_query
35 35 sort_init 'id', 'desc'
36 36 sort_update(@query.sortable_columns)
37 37
38 38 if @query.valid?
39 39 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
40 40 :limit => 25)
41 41 end
42 42 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
43 43 render :layout => false, :content_type => 'application/atom+xml'
44 44 rescue ActiveRecord::RecordNotFound
45 45 render_404
46 46 end
47 47
48 48 def diff
49 49 @issue = @journal.issue
50 50 if params[:detail_id].present?
51 51 @detail = @journal.details.find_by_id(params[:detail_id])
52 52 else
53 53 @detail = @journal.details.detect {|d| d.prop_key == 'description'}
54 54 end
55 55 (render_404; return false) unless @issue && @detail
56 56 @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
57 57 end
58 58
59 59 def new
60 60 @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
61 61 if @journal
62 62 user = @journal.user
63 63 text = @journal.notes
64 64 else
65 65 user = @issue.author
66 66 text = @issue.description
67 67 end
68 68 # Replaces pre blocks with [...]
69 69 text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
70 70 @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
71 71 @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
72 72 rescue ActiveRecord::RecordNotFound
73 73 render_404
74 74 end
75 75
76 76 def edit
77 77 (render_403; return false) unless @journal.editable_by?(User.current)
78 78 if request.post?
79 79 @journal.update_attributes(:notes => params[:notes]) if params[:notes]
80 80 @journal.destroy if @journal.details.empty? && @journal.notes.blank?
81 81 call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
82 82 respond_to do |format|
83 83 format.html { redirect_to issue_path(@journal.journalized) }
84 84 format.js { render :action => 'update' }
85 85 end
86 86 else
87 87 respond_to do |format|
88 88 format.html {
89 89 # TODO: implement non-JS journal update
90 90 render :nothing => true
91 91 }
92 92 format.js
93 93 end
94 94 end
95 95 end
96 96
97 97 private
98 98
99 99 def find_journal
100 100 @journal = Journal.visible.find(params[:id])
101 101 @project = @journal.journalized.project
102 102 rescue ActiveRecord::RecordNotFound
103 103 render_404
104 104 end
105 105 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandlerController < ActionController::Base
19 19 before_filter :check_credential
20 20
21 21 # Submits an incoming email to MailHandler
22 22 def index
23 23 options = params.dup
24 24 email = options.delete(:email)
25 25 if MailHandler.receive(email, options)
26 26 render :nothing => true, :status => :created
27 27 else
28 28 render :nothing => true, :status => :unprocessable_entity
29 29 end
30 30 end
31 31
32 32 private
33 33
34 34 def check_credential
35 35 User.current = nil
36 36 unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
37 37 render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
38 38 end
39 39 end
40 40 end
@@ -1,124 +1,124
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MembersController < ApplicationController
19 19 model_object Member
20 20 before_filter :find_model_object, :except => [:index, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
23 23 before_filter :authorize
24 24 accept_api_auth :index, :show, :create, :update, :destroy
25 25
26 26 def index
27 27 @offset, @limit = api_offset_and_limit
28 28 @member_count = @project.member_principals.count
29 29 @member_pages = Paginator.new @member_count, @limit, params['page']
30 30 @offset ||= @member_pages.offset
31 31 @members = @project.member_principals.all(
32 32 :order => "#{Member.table_name}.id",
33 33 :limit => @limit,
34 34 :offset => @offset
35 35 )
36 36
37 37 respond_to do |format|
38 38 format.html { head 406 }
39 39 format.api
40 40 end
41 41 end
42 42
43 43 def show
44 44 respond_to do |format|
45 45 format.html { head 406 }
46 46 format.api
47 47 end
48 48 end
49 49
50 50 def create
51 51 members = []
52 52 if params[:membership]
53 53 if params[:membership][:user_ids]
54 54 attrs = params[:membership].dup
55 55 user_ids = attrs.delete(:user_ids)
56 56 user_ids.each do |user_id|
57 57 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
58 58 end
59 59 else
60 60 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
61 61 end
62 62 @project.members << members
63 63 end
64 64
65 65 respond_to do |format|
66 66 format.html { redirect_to_settings_in_projects }
67 67 format.js { @members = members }
68 68 format.api {
69 69 @member = members.first
70 70 if @member.valid?
71 71 render :action => 'show', :status => :created, :location => membership_url(@member)
72 72 else
73 73 render_validation_errors(@member)
74 74 end
75 75 }
76 76 end
77 77 end
78 78
79 79 def update
80 80 if params[:membership]
81 81 @member.role_ids = params[:membership][:role_ids]
82 82 end
83 83 saved = @member.save
84 84 respond_to do |format|
85 85 format.html { redirect_to_settings_in_projects }
86 86 format.js
87 87 format.api {
88 88 if saved
89 89 render_api_ok
90 90 else
91 91 render_validation_errors(@member)
92 92 end
93 93 }
94 94 end
95 95 end
96 96
97 97 def destroy
98 98 if request.delete? && @member.deletable?
99 99 @member.destroy
100 100 end
101 101 respond_to do |format|
102 102 format.html { redirect_to_settings_in_projects }
103 103 format.js
104 104 format.api {
105 105 if @member.destroyed?
106 106 render_api_ok
107 107 else
108 108 head :unprocessable_entity
109 109 end
110 110 }
111 111 end
112 112 end
113 113
114 114 def autocomplete
115 115 @principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
116 116 render :layout => false
117 117 end
118 118
119 119 private
120 120
121 121 def redirect_to_settings_in_projects
122 122 redirect_to settings_project_path(@project, :tab => 'members')
123 123 end
124 124 end
@@ -1,141 +1,141
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
39 39 page = 1 + offset / REPLIES_PER_PAGE
40 40 end
41 41
42 42 @reply_count = @topic.children.count
43 43 @reply_pages = Paginator.new @reply_count, REPLIES_PER_PAGE, page
44 44 @replies = @topic.children.
45 45 includes(:author, :attachments, {:board => :project}).
46 46 reorder("#{Message.table_name}.created_on ASC").
47 47 limit(@reply_pages.items_per_page).
48 48 offset(@reply_pages.offset).
49 49 all
50 50
51 51 @reply = Message.new(:subject => "RE: #{@message.subject}")
52 52 render :action => "show", :layout => false if request.xhr?
53 53 end
54 54
55 55 # Create a new topic
56 56 def new
57 57 @message = Message.new
58 58 @message.author = User.current
59 59 @message.board = @board
60 60 @message.safe_attributes = params[:message]
61 61 if request.post?
62 62 @message.save_attachments(params[:attachments])
63 63 if @message.save
64 64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
65 65 render_attachment_warning_if_needed(@message)
66 66 redirect_to board_message_path(@board, @message)
67 67 end
68 68 end
69 69 end
70 70
71 71 # Reply to a topic
72 72 def reply
73 73 @reply = Message.new
74 74 @reply.author = User.current
75 75 @reply.board = @board
76 76 @reply.safe_attributes = params[:reply]
77 77 @topic.children << @reply
78 78 if !@reply.new_record?
79 79 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
80 80 attachments = Attachment.attach_files(@reply, params[:attachments])
81 81 render_attachment_warning_if_needed(@reply)
82 82 end
83 83 redirect_to board_message_path(@board, @topic, :r => @reply)
84 84 end
85 85
86 86 # Edit a message
87 87 def edit
88 88 (render_403; return false) unless @message.editable_by?(User.current)
89 89 @message.safe_attributes = params[:message]
90 90 if request.post? && @message.save
91 91 attachments = Attachment.attach_files(@message, params[:attachments])
92 92 render_attachment_warning_if_needed(@message)
93 93 flash[:notice] = l(:notice_successful_update)
94 94 @message.reload
95 95 redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id))
96 96 end
97 97 end
98 98
99 99 # Delete a messages
100 100 def destroy
101 101 (render_403; return false) unless @message.destroyable_by?(User.current)
102 102 r = @message.to_param
103 103 @message.destroy
104 104 if @message.parent
105 105 redirect_to board_message_path(@board, @message.parent, :r => r)
106 106 else
107 107 redirect_to project_board_path(@project, @board)
108 108 end
109 109 end
110 110
111 111 def quote
112 112 @subject = @message.subject
113 113 @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:')
114 114
115 115 @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> "
116 116 @content << @message.content.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
117 117 end
118 118
119 119 def preview
120 120 message = @board.messages.find_by_id(params[:id])
121 121 @text = (params[:message] || params[:reply])[:content]
122 122 @previewed = message
123 123 render :partial => 'common/preview'
124 124 end
125 125
126 126 private
127 127 def find_message
128 128 find_board
129 129 @message = @board.messages.find(params[:id], :include => :parent)
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.find(params[:board_id], :include => :project)
137 137 @project = @board.project
138 138 rescue ActiveRecord::RecordNotFound
139 139 render_404
140 140 end
141 141 end
@@ -1,197 +1,197
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
21 21 helper :issues
22 22 helper :users
23 23 helper :custom_fields
24 24
25 25 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
26 26 'issuesreportedbyme' => :label_reported_issues,
27 27 'issueswatched' => :label_watched_issues,
28 28 'news' => :label_news_latest,
29 29 'calendar' => :label_calendar,
30 30 'documents' => :label_document_plural,
31 31 'timelog' => :label_spent_time
32 32 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
33 33
34 34 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
35 35 'right' => ['issuesreportedbyme']
36 36 }.freeze
37 37
38 38 def index
39 39 page
40 40 render :action => 'page'
41 41 end
42 42
43 43 # Show user's page
44 44 def page
45 45 @user = User.current
46 46 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
47 47 end
48 48
49 49 # Edit user's account
50 50 def account
51 51 @user = User.current
52 52 @pref = @user.pref
53 53 if request.post?
54 54 @user.safe_attributes = params[:user]
55 55 @user.pref.attributes = params[:pref]
56 56 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
57 57 if @user.save
58 58 @user.pref.save
59 59 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
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 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
97 97 if @user.save
98 98 flash[:notice] = l(:notice_account_password_updated)
99 99 redirect_to my_account_path
100 100 end
101 101 else
102 102 flash[:error] = l(:notice_account_wrong_password)
103 103 end
104 104 end
105 105 end
106 106
107 107 # Create a new feeds key
108 108 def reset_rss_key
109 109 if request.post?
110 110 if User.current.rss_token
111 111 User.current.rss_token.destroy
112 112 User.current.reload
113 113 end
114 114 User.current.rss_key
115 115 flash[:notice] = l(:notice_feeds_access_key_reseted)
116 116 end
117 117 redirect_to my_account_path
118 118 end
119 119
120 120 # Create a new API key
121 121 def reset_api_key
122 122 if request.post?
123 123 if User.current.api_token
124 124 User.current.api_token.destroy
125 125 User.current.reload
126 126 end
127 127 User.current.api_key
128 128 flash[:notice] = l(:notice_api_access_key_reseted)
129 129 end
130 130 redirect_to my_account_path
131 131 end
132 132
133 133 # User's page layout configuration
134 134 def page_layout
135 135 @user = User.current
136 136 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
137 137 @block_options = []
138 138 BLOCKS.each do |k, v|
139 139 unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)}
140 140 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
141 141 end
142 142 end
143 143 end
144 144
145 145 # Add a block to user's page
146 146 # The block is added on top of the page
147 147 # params[:block] : id of the block to add
148 148 def add_block
149 149 block = params[:block].to_s.underscore
150 150 (render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
151 151 @user = User.current
152 152 layout = @user.pref[:my_page_layout] || {}
153 153 # remove if already present in a group
154 154 %w(top left right).each {|f| (layout[f] ||= []).delete block }
155 155 # add it on top
156 156 layout['top'].unshift block
157 157 @user.pref[:my_page_layout] = layout
158 158 @user.pref.save
159 159 redirect_to my_page_layout_path
160 160 end
161 161
162 162 # Remove a block to user's page
163 163 # params[:block] : id of the block to remove
164 164 def remove_block
165 165 block = params[:block].to_s.underscore
166 166 @user = User.current
167 167 # remove block in all groups
168 168 layout = @user.pref[:my_page_layout] || {}
169 169 %w(top left right).each {|f| (layout[f] ||= []).delete block }
170 170 @user.pref[:my_page_layout] = layout
171 171 @user.pref.save
172 172 redirect_to my_page_layout_path
173 173 end
174 174
175 175 # Change blocks order on user's page
176 176 # params[:group] : group to order (top, left or right)
177 177 # params[:list-(top|left|right)] : array of block ids of the group
178 178 def order_blocks
179 179 group = params[:group]
180 180 @user = User.current
181 181 if group.is_a?(String)
182 182 group_items = (params["blocks"] || []).collect(&:underscore)
183 183 group_items.each {|s| s.sub!(/^block_/, '')}
184 184 if group_items and group_items.is_a? Array
185 185 layout = @user.pref[:my_page_layout] || {}
186 186 # remove group blocks if they are presents in other groups
187 187 %w(top left right).each {|f|
188 188 layout[f] = (layout[f] || []) - group_items
189 189 }
190 190 layout[group] = group_items
191 191 @user.pref[:my_page_layout] = layout
192 192 @user.pref.save
193 193 end
194 194 end
195 195 render :nothing => true
196 196 end
197 197 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.all(:include => [:author, :project],
46 46 :order => "#{News.table_name}.created_on DESC",
47 47 :offset => @offset,
48 48 :limit => @limit)
49 49
50 50 respond_to do |format|
51 51 format.html {
52 52 @news = News.new # for adding news inline
53 53 render :layout => false if request.xhr?
54 54 }
55 55 format.api
56 56 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
57 57 end
58 58 end
59 59
60 60 def show
61 61 @comments = @news.comments
62 62 @comments.reverse! if User.current.wants_comments_in_reverse_order?
63 63 end
64 64
65 65 def new
66 66 @news = News.new(:project => @project, :author => User.current)
67 67 end
68 68
69 69 def create
70 70 @news = News.new(:project => @project, :author => User.current)
71 71 @news.safe_attributes = params[:news]
72 72 @news.save_attachments(params[:attachments])
73 73 if @news.save
74 74 render_attachment_warning_if_needed(@news)
75 75 flash[:notice] = l(:notice_successful_create)
76 76 redirect_to project_news_index_path(@project)
77 77 else
78 78 render :action => 'new'
79 79 end
80 80 end
81 81
82 82 def edit
83 83 end
84 84
85 85 def update
86 86 @news.safe_attributes = params[:news]
87 87 @news.save_attachments(params[:attachments])
88 88 if @news.save
89 89 render_attachment_warning_if_needed(@news)
90 90 flash[:notice] = l(:notice_successful_update)
91 91 redirect_to news_path(@news)
92 92 else
93 93 render :action => 'edit'
94 94 end
95 95 end
96 96
97 97 def destroy
98 98 @news.destroy
99 99 redirect_to project_news_index_path(@project)
100 100 end
101 101
102 102 private
103 103
104 104 def find_optional_project
105 105 return true unless params[:project_id]
106 106 @project = Project.find(params[:project_id])
107 107 authorize
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,53 +1,53
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class PreviewsController < ApplicationController
19 19 before_filter :find_project, :find_attachments
20 20
21 21 def issue
22 22 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
23 23 if @issue
24 24 @description = params[:issue] && params[:issue][:description]
25 25 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
26 26 @description = nil
27 27 end
28 28 # params[:notes] is useful for preview of notes in issue history
29 29 @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil)
30 30 else
31 31 @description = (params[:issue] ? params[:issue][:description] : nil)
32 32 end
33 33 render :layout => false
34 34 end
35 35
36 36 def news
37 37 if params[:id].present? && news = News.visible.find_by_id(params[:id])
38 38 @previewed = news
39 39 end
40 40 @text = (params[:news] ? params[:news][:description] : nil)
41 41 render :partial => 'common/preview'
42 42 end
43 43
44 44 private
45 45
46 46 def find_project
47 47 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
48 48 @project = Project.find(project_id)
49 49 rescue ActiveRecord::RecordNotFound
50 50 render_404
51 51 end
52 52
53 53 end
@@ -1,42 +1,42
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectEnumerationsController < ApplicationController
19 19 before_filter :find_project_by_project_id
20 20 before_filter :authorize
21 21
22 22 def update
23 23 if request.put? && params[:enumerations]
24 24 Project.transaction do
25 25 params[:enumerations].each do |id, activity|
26 26 @project.update_or_create_time_entry_activity(id, activity)
27 27 end
28 28 end
29 29 flash[:notice] = l(:notice_successful_update)
30 30 end
31 31
32 32 redirect_to settings_project_path(@project, :tab => 'activities')
33 33 end
34 34
35 35 def destroy
36 36 @project.time_entry_activities.each do |time_entry_activity|
37 37 time_entry_activity.destroy(time_entry_activity.parent)
38 38 end
39 39 flash[:notice] = l(:notice_successful_update)
40 40 redirect_to settings_project_path(@project, :tab => 'activities')
41 41 end
42 42 end
@@ -1,261 +1,261
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :roadmap, :only => :roadmap
21 21 menu_item :settings, :only => :settings
22 22
23 23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 25 before_filter :authorize_global, :only => [:new, :create]
26 26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 27 accept_rss_auth :index
28 28 accept_api_auth :index, :show, :create, :update, :destroy
29 29
30 30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
31 31 if controller.request.post?
32 32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
33 33 end
34 34 end
35 35
36 36 helper :sort
37 37 include SortHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issues
41 41 helper :queries
42 42 include QueriesHelper
43 43 helper :repositories
44 44 include RepositoriesHelper
45 45 include ProjectsHelper
46 46
47 47 # Lists visible projects
48 48 def index
49 49 respond_to do |format|
50 50 format.html {
51 51 scope = Project
52 52 unless params[:closed]
53 53 scope = scope.active
54 54 end
55 55 @projects = scope.visible.order('lft').all
56 56 }
57 57 format.api {
58 58 @offset, @limit = api_offset_and_limit
59 59 @project_count = Project.visible.count
60 60 @projects = Project.visible.offset(@offset).limit(@limit).order('lft').all
61 61 }
62 62 format.atom {
63 63 projects = Project.visible.order('created_on DESC').limit(Setting.feeds_limit.to_i).all
64 64 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
65 65 }
66 66 end
67 67 end
68 68
69 69 def new
70 70 @issue_custom_fields = IssueCustomField.sorted.all
71 71 @trackers = Tracker.sorted.all
72 72 @project = Project.new
73 73 @project.safe_attributes = params[:project]
74 74 end
75 75
76 76 def create
77 77 @issue_custom_fields = IssueCustomField.sorted.all
78 78 @trackers = Tracker.sorted.all
79 79 @project = Project.new
80 80 @project.safe_attributes = params[:project]
81 81
82 82 if validate_parent_id && @project.save
83 83 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
84 84 # Add current user as a project member if he is not admin
85 85 unless User.current.admin?
86 86 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
87 87 m = Member.new(:user => User.current, :roles => [r])
88 88 @project.members << m
89 89 end
90 90 respond_to do |format|
91 91 format.html {
92 92 flash[:notice] = l(:notice_successful_create)
93 93 if params[:continue]
94 94 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
95 95 redirect_to new_project_path(attrs)
96 96 else
97 97 redirect_to settings_project_path(@project)
98 98 end
99 99 }
100 100 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
101 101 end
102 102 else
103 103 respond_to do |format|
104 104 format.html { render :action => 'new' }
105 105 format.api { render_validation_errors(@project) }
106 106 end
107 107 end
108 108 end
109 109
110 110 def copy
111 111 @issue_custom_fields = IssueCustomField.sorted.all
112 112 @trackers = Tracker.sorted.all
113 113 @source_project = Project.find(params[:id])
114 114 if request.get?
115 115 @project = Project.copy_from(@source_project)
116 116 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
117 117 else
118 118 Mailer.with_deliveries(params[:notifications] == '1') do
119 119 @project = Project.new
120 120 @project.safe_attributes = params[:project]
121 121 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
122 122 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
123 123 flash[:notice] = l(:notice_successful_create)
124 124 redirect_to settings_project_path(@project)
125 125 elsif !@project.new_record?
126 126 # Project was created
127 127 # But some objects were not copied due to validation failures
128 128 # (eg. issues from disabled trackers)
129 129 # TODO: inform about that
130 130 redirect_to settings_project_path(@project)
131 131 end
132 132 end
133 133 end
134 134 rescue ActiveRecord::RecordNotFound
135 135 # source_project not found
136 136 render_404
137 137 end
138 138
139 139 # Show @project
140 140 def show
141 141 if params[:jump]
142 142 # try to redirect to the requested menu item
143 143 redirect_to_project_menu_item(@project, params[:jump]) && return
144 144 end
145 145
146 146 @users_by_role = @project.users_by_role
147 147 @subprojects = @project.children.visible.all
148 148 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").all
149 149 @trackers = @project.rolled_up_trackers
150 150
151 151 cond = @project.project_condition(Setting.display_subprojects_issues?)
152 152
153 153 @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
154 154 @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
155 155
156 156 if User.current.allowed_to?(:view_time_entries, @project)
157 157 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
158 158 end
159 159
160 160 @key = User.current.rss_key
161 161
162 162 respond_to do |format|
163 163 format.html
164 164 format.api
165 165 end
166 166 end
167 167
168 168 def settings
169 169 @issue_custom_fields = IssueCustomField.sorted.all
170 170 @issue_category ||= IssueCategory.new
171 171 @member ||= @project.members.new
172 172 @trackers = Tracker.sorted.all
173 173 @wiki ||= @project.wiki
174 174 end
175 175
176 176 def edit
177 177 end
178 178
179 179 def update
180 180 @project.safe_attributes = params[:project]
181 181 if validate_parent_id && @project.save
182 182 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
183 183 respond_to do |format|
184 184 format.html {
185 185 flash[:notice] = l(:notice_successful_update)
186 186 redirect_to settings_project_path(@project)
187 187 }
188 188 format.api { render_api_ok }
189 189 end
190 190 else
191 191 respond_to do |format|
192 192 format.html {
193 193 settings
194 194 render :action => 'settings'
195 195 }
196 196 format.api { render_validation_errors(@project) }
197 197 end
198 198 end
199 199 end
200 200
201 201 def modules
202 202 @project.enabled_module_names = params[:enabled_module_names]
203 203 flash[:notice] = l(:notice_successful_update)
204 204 redirect_to settings_project_path(@project, :tab => 'modules')
205 205 end
206 206
207 207 def archive
208 208 if request.post?
209 209 unless @project.archive
210 210 flash[:error] = l(:error_can_not_archive_project)
211 211 end
212 212 end
213 213 redirect_to admin_projects_path(:status => params[:status])
214 214 end
215 215
216 216 def unarchive
217 217 @project.unarchive if request.post? && !@project.active?
218 218 redirect_to admin_projects_path(:status => params[:status])
219 219 end
220 220
221 221 def close
222 222 @project.close
223 223 redirect_to project_path(@project)
224 224 end
225 225
226 226 def reopen
227 227 @project.reopen
228 228 redirect_to project_path(@project)
229 229 end
230 230
231 231 # Delete @project
232 232 def destroy
233 233 @project_to_destroy = @project
234 234 if api_request? || params[:confirm]
235 235 @project_to_destroy.destroy
236 236 respond_to do |format|
237 237 format.html { redirect_to admin_projects_path }
238 238 format.api { render_api_ok }
239 239 end
240 240 end
241 241 # hide project in layout
242 242 @project = nil
243 243 end
244 244
245 245 private
246 246
247 247 # Validates parent_id param according to user's permissions
248 248 # TODO: move it to Project model in a validation that depends on User.current
249 249 def validate_parent_id
250 250 return true if User.current.admin?
251 251 parent_id = params[:project] && params[:project][:parent_id]
252 252 if parent_id || @project.new_record?
253 253 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
254 254 unless @project.allowed_parents.include?(parent)
255 255 @project.errors.add :parent_id, :invalid
256 256 return false
257 257 end
258 258 end
259 259 true
260 260 end
261 261 end
@@ -1,106 +1,106
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
35 35 @query_count = IssueQuery.visible.count
36 36 @query_pages = Paginator.new @query_count, @limit, params['page']
37 37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
38 38
39 39 respond_to do |format|
40 40 format.api
41 41 end
42 42 end
43 43
44 44 def new
45 45 @query = IssueQuery.new
46 46 @query.user = User.current
47 47 @query.project = @project
48 48 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
49 49 @query.build_from_params(params)
50 50 end
51 51
52 52 def create
53 53 @query = IssueQuery.new(params[:query])
54 54 @query.user = User.current
55 55 @query.project = params[:query_is_for_all] ? nil : @project
56 56 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
57 57 @query.build_from_params(params)
58 58 @query.column_names = nil if params[:default_columns]
59 59
60 60 if @query.save
61 61 flash[:notice] = l(:notice_successful_create)
62 62 redirect_to _project_issues_path(@project, :query_id => @query)
63 63 else
64 64 render :action => 'new', :layout => !request.xhr?
65 65 end
66 66 end
67 67
68 68 def edit
69 69 end
70 70
71 71 def update
72 72 @query.attributes = params[:query]
73 73 @query.project = nil if params[:query_is_for_all]
74 74 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
75 75 @query.build_from_params(params)
76 76 @query.column_names = nil if params[:default_columns]
77 77
78 78 if @query.save
79 79 flash[:notice] = l(:notice_successful_update)
80 80 redirect_to _project_issues_path(@project, :query_id => @query)
81 81 else
82 82 render :action => 'edit'
83 83 end
84 84 end
85 85
86 86 def destroy
87 87 @query.destroy
88 88 redirect_to _project_issues_path(@project, :set_filter => 1)
89 89 end
90 90
91 91 private
92 92 def find_query
93 93 @query = IssueQuery.find(params[:id])
94 94 @project = @query.project
95 95 render_403 unless @query.editable_by?(User.current)
96 96 rescue ActiveRecord::RecordNotFound
97 97 render_404
98 98 end
99 99
100 100 def find_optional_project
101 101 @project = Project.find(params[:project_id]) if params[:project_id]
102 102 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
103 103 rescue ActiveRecord::RecordNotFound
104 104 render_404
105 105 end
106 106 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ReportsController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :authorize, :find_issue_statuses
21 21
22 22 def issue_report
23 23 @trackers = @project.trackers
24 24 @versions = @project.shared_versions.sort
25 25 @priorities = IssuePriority.all.reverse
26 26 @categories = @project.issue_categories
27 27 @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
28 28 @authors = @project.users.sort
29 29 @subprojects = @project.descendants.visible
30 30
31 31 @issues_by_tracker = Issue.by_tracker(@project)
32 32 @issues_by_version = Issue.by_version(@project)
33 33 @issues_by_priority = Issue.by_priority(@project)
34 34 @issues_by_category = Issue.by_category(@project)
35 35 @issues_by_assigned_to = Issue.by_assigned_to(@project)
36 36 @issues_by_author = Issue.by_author(@project)
37 37 @issues_by_subproject = Issue.by_subproject(@project) || []
38 38
39 39 render :template => "reports/issue_report"
40 40 end
41 41
42 42 def issue_report_details
43 43 case params[:detail]
44 44 when "tracker"
45 45 @field = "tracker_id"
46 46 @rows = @project.trackers
47 47 @data = Issue.by_tracker(@project)
48 48 @report_title = l(:field_tracker)
49 49 when "version"
50 50 @field = "fixed_version_id"
51 51 @rows = @project.shared_versions.sort
52 52 @data = Issue.by_version(@project)
53 53 @report_title = l(:field_version)
54 54 when "priority"
55 55 @field = "priority_id"
56 56 @rows = IssuePriority.all.reverse
57 57 @data = Issue.by_priority(@project)
58 58 @report_title = l(:field_priority)
59 59 when "category"
60 60 @field = "category_id"
61 61 @rows = @project.issue_categories
62 62 @data = Issue.by_category(@project)
63 63 @report_title = l(:field_category)
64 64 when "assigned_to"
65 65 @field = "assigned_to_id"
66 66 @rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
67 67 @data = Issue.by_assigned_to(@project)
68 68 @report_title = l(:field_assigned_to)
69 69 when "author"
70 70 @field = "author_id"
71 71 @rows = @project.users.sort
72 72 @data = Issue.by_author(@project)
73 73 @report_title = l(:field_author)
74 74 when "subproject"
75 75 @field = "project_id"
76 76 @rows = @project.descendants.visible
77 77 @data = Issue.by_subproject(@project) || []
78 78 @report_title = l(:field_subproject)
79 79 end
80 80
81 81 respond_to do |format|
82 82 if @field
83 83 format.html {}
84 84 else
85 85 format.html { redirect_to :action => 'issue_report', :id => @project }
86 86 end
87 87 end
88 88 end
89 89
90 90 private
91 91
92 92 def find_issue_statuses
93 93 @statuses = IssueStatus.sorted.all
94 94 end
95 95 end
@@ -1,434 +1,434
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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/abstract_adapter'
22 22
23 23 class ChangesetNotFound < Exception; end
24 24 class InvalidRevisionParam < Exception; end
25 25
26 26 class RepositoriesController < ApplicationController
27 27 menu_item :repository
28 28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 29 default_search_scope :changesets
30 30
31 31 before_filter :find_project_by_project_id, :only => [:new, :create]
32 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 35 before_filter :authorize
36 36 accept_rss_auth :revisions
37 37
38 38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39 39
40 40 def new
41 41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
42 42 @repository = Repository.factory(scm)
43 43 @repository.is_default = @project.repository.nil?
44 44 @repository.project = @project
45 45 end
46 46
47 47 def create
48 48 attrs = pickup_extra_info
49 49 @repository = Repository.factory(params[:repository_scm])
50 50 @repository.safe_attributes = params[:repository]
51 51 if attrs[:attrs_extra].keys.any?
52 52 @repository.merge_extra_info(attrs[:attrs_extra])
53 53 end
54 54 @repository.project = @project
55 55 if request.post? && @repository.save
56 56 redirect_to settings_project_path(@project, :tab => 'repositories')
57 57 else
58 58 render :action => 'new'
59 59 end
60 60 end
61 61
62 62 def edit
63 63 end
64 64
65 65 def update
66 66 attrs = pickup_extra_info
67 67 @repository.safe_attributes = attrs[:attrs]
68 68 if attrs[:attrs_extra].keys.any?
69 69 @repository.merge_extra_info(attrs[:attrs_extra])
70 70 end
71 71 @repository.project = @project
72 72 if request.put? && @repository.save
73 73 redirect_to settings_project_path(@project, :tab => 'repositories')
74 74 else
75 75 render :action => 'edit'
76 76 end
77 77 end
78 78
79 79 def pickup_extra_info
80 80 p = {}
81 81 p_extra = {}
82 82 params[:repository].each do |k, v|
83 83 if k =~ /^extra_/
84 84 p_extra[k] = v
85 85 else
86 86 p[k] = v
87 87 end
88 88 end
89 89 {:attrs => p, :attrs_extra => p_extra}
90 90 end
91 91 private :pickup_extra_info
92 92
93 93 def committers
94 94 @committers = @repository.committers
95 95 @users = @project.users
96 96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 97 @users += User.find_all_by_id(additional_user_ids) 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 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.items_per_page).
146 146 offset(@changeset_pages.offset).
147 147 includes(:user, :repository, :parents).
148 148 all
149 149
150 150 respond_to do |format|
151 151 format.html { render :layout => false if request.xhr? }
152 152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
153 153 end
154 154 end
155 155
156 156 def raw
157 157 entry_and_raw(true)
158 158 end
159 159
160 160 def entry
161 161 entry_and_raw(false)
162 162 end
163 163
164 164 def entry_and_raw(is_raw)
165 165 @entry = @repository.entry(@path, @rev)
166 166 (show_error_not_found; return) unless @entry
167 167
168 168 # If the entry is a dir, show the browser
169 169 (show; return) if @entry.is_dir?
170 170
171 171 @content = @repository.cat(@path, @rev)
172 172 (show_error_not_found; return) unless @content
173 173 if is_raw ||
174 174 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
175 175 ! is_entry_text_data?(@content, @path)
176 176 # Force the download
177 177 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
178 178 send_type = Redmine::MimeType.of(@path)
179 179 send_opt[:type] = send_type.to_s if send_type
180 180 send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
181 181 send_data @content, send_opt
182 182 else
183 183 # Prevent empty lines when displaying a file with Windows style eol
184 184 # TODO: UTF-16
185 185 # Is this needs? AttachmentsController reads file simply.
186 186 @content.gsub!("\r\n", "\n")
187 187 @changeset = @repository.find_changeset_by_name(@rev)
188 188 end
189 189 end
190 190 private :entry_and_raw
191 191
192 192 def is_entry_text_data?(ent, path)
193 193 # UTF-16 contains "\x00".
194 194 # It is very strict that file contains less than 30% of ascii symbols
195 195 # in non Western Europe.
196 196 return true if Redmine::MimeType.is_type?('text', path)
197 197 # Ruby 1.8.6 has a bug of integer divisions.
198 198 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
199 199 return false if ent.is_binary_data?
200 200 true
201 201 end
202 202 private :is_entry_text_data?
203 203
204 204 def annotate
205 205 @entry = @repository.entry(@path, @rev)
206 206 (show_error_not_found; return) unless @entry
207 207
208 208 @annotate = @repository.scm.annotate(@path, @rev)
209 209 if @annotate.nil? || @annotate.empty?
210 210 (render_error l(:error_scm_annotate); return)
211 211 end
212 212 ann_buf_size = 0
213 213 @annotate.lines.each do |buf|
214 214 ann_buf_size += buf.size
215 215 end
216 216 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
217 217 (render_error l(:error_scm_annotate_big_text_file); return)
218 218 end
219 219 @changeset = @repository.find_changeset_by_name(@rev)
220 220 end
221 221
222 222 def revision
223 223 respond_to do |format|
224 224 format.html
225 225 format.js {render :layout => false}
226 226 end
227 227 end
228 228
229 229 # Adds a related issue to a changeset
230 230 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
231 231 def add_related_issue
232 232 @issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
233 233 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
234 234 @issue = nil
235 235 end
236 236
237 237 if @issue
238 238 @changeset.issues << @issue
239 239 end
240 240 end
241 241
242 242 # Removes a related issue from a changeset
243 243 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
244 244 def remove_related_issue
245 245 @issue = Issue.visible.find_by_id(params[:issue_id])
246 246 if @issue
247 247 @changeset.issues.delete(@issue)
248 248 end
249 249 end
250 250
251 251 def diff
252 252 if params[:format] == 'diff'
253 253 @diff = @repository.diff(@path, @rev, @rev_to)
254 254 (show_error_not_found; return) unless @diff
255 255 filename = "changeset_r#{@rev}"
256 256 filename << "_r#{@rev_to}" if @rev_to
257 257 send_data @diff.join, :filename => "#{filename}.diff",
258 258 :type => 'text/x-patch',
259 259 :disposition => 'attachment'
260 260 else
261 261 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
262 262 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
263 263
264 264 # Save diff type as user preference
265 265 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
266 266 User.current.pref[:diff_type] = @diff_type
267 267 User.current.preference.save
268 268 end
269 269 @cache_key = "repositories/diff/#{@repository.id}/" +
270 270 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
271 271 unless read_fragment(@cache_key)
272 272 @diff = @repository.diff(@path, @rev, @rev_to)
273 273 show_error_not_found unless @diff
274 274 end
275 275
276 276 @changeset = @repository.find_changeset_by_name(@rev)
277 277 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
278 278 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
279 279 end
280 280 end
281 281
282 282 def stats
283 283 end
284 284
285 285 def graph
286 286 data = nil
287 287 case params[:graph]
288 288 when "commits_per_month"
289 289 data = graph_commits_per_month(@repository)
290 290 when "commits_per_author"
291 291 data = graph_commits_per_author(@repository)
292 292 end
293 293 if data
294 294 headers["Content-Type"] = "image/svg+xml"
295 295 send_data(data, :type => "image/svg+xml", :disposition => "inline")
296 296 else
297 297 render_404
298 298 end
299 299 end
300 300
301 301 private
302 302
303 303 def find_repository
304 304 @repository = Repository.find(params[:id])
305 305 @project = @repository.project
306 306 rescue ActiveRecord::RecordNotFound
307 307 render_404
308 308 end
309 309
310 310 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
311 311
312 312 def find_project_repository
313 313 @project = Project.find(params[:id])
314 314 if params[:repository_id].present?
315 315 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
316 316 else
317 317 @repository = @project.repository
318 318 end
319 319 (render_404; return false) unless @repository
320 320 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
321 321 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
322 322 @rev_to = params[:rev_to]
323 323
324 324 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
325 325 if @repository.branches.blank?
326 326 raise InvalidRevisionParam
327 327 end
328 328 end
329 329 rescue ActiveRecord::RecordNotFound
330 330 render_404
331 331 rescue InvalidRevisionParam
332 332 show_error_not_found
333 333 end
334 334
335 335 def find_changeset
336 336 if @rev.present?
337 337 @changeset = @repository.find_changeset_by_name(@rev)
338 338 end
339 339 show_error_not_found unless @changeset
340 340 end
341 341
342 342 def show_error_not_found
343 343 render_error :message => l(:error_scm_not_found), :status => 404
344 344 end
345 345
346 346 # Handler for Redmine::Scm::Adapters::CommandFailed exception
347 347 def show_error_command_failed(exception)
348 348 render_error l(:error_scm_command_failed, exception.message)
349 349 end
350 350
351 351 def graph_commits_per_month(repository)
352 352 @date_to = Date.today
353 353 @date_from = @date_to << 11
354 354 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
355 355 commits_by_day = Changeset.count(
356 356 :all, :group => :commit_date,
357 357 :conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
358 358 commits_by_month = [0] * 12
359 359 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
360 360
361 361 changes_by_day = Change.count(
362 362 :all, :group => :commit_date, :include => :changeset,
363 363 :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
364 364 changes_by_month = [0] * 12
365 365 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
366 366
367 367 fields = []
368 368 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
369 369
370 370 graph = SVG::Graph::Bar.new(
371 371 :height => 300,
372 372 :width => 800,
373 373 :fields => fields.reverse,
374 374 :stack => :side,
375 375 :scale_integers => true,
376 376 :step_x_labels => 2,
377 377 :show_data_values => false,
378 378 :graph_title => l(:label_commits_per_month),
379 379 :show_graph_title => true
380 380 )
381 381
382 382 graph.add_data(
383 383 :data => commits_by_month[0..11].reverse,
384 384 :title => l(:label_revision_plural)
385 385 )
386 386
387 387 graph.add_data(
388 388 :data => changes_by_month[0..11].reverse,
389 389 :title => l(:label_change_plural)
390 390 )
391 391
392 392 graph.burn
393 393 end
394 394
395 395 def graph_commits_per_author(repository)
396 396 commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
397 397 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
398 398
399 399 changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
400 400 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
401 401
402 402 fields = commits_by_author.collect {|r| r.first}
403 403 commits_data = commits_by_author.collect {|r| r.last}
404 404 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
405 405
406 406 fields = fields + [""]*(10 - fields.length) if fields.length<10
407 407 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
408 408 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
409 409
410 410 # Remove email adress in usernames
411 411 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
412 412
413 413 graph = SVG::Graph::BarHorizontal.new(
414 414 :height => 400,
415 415 :width => 800,
416 416 :fields => fields,
417 417 :stack => :side,
418 418 :scale_integers => true,
419 419 :show_data_values => false,
420 420 :rotate_y_labels => false,
421 421 :graph_title => l(:label_commits_per_author),
422 422 :show_graph_title => true
423 423 )
424 424 graph.add_data(
425 425 :data => commits_data,
426 426 :title => l(:label_revision_plural)
427 427 )
428 428 graph.add_data(
429 429 :data => changes_data,
430 430 :title => l(:label_change_plural)
431 431 )
432 432 graph.burn
433 433 end
434 434 end
@@ -1,108 +1,108
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class RolesController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => [:index, :show]
22 22 before_filter :require_admin_or_api_request, :only => [:index, :show]
23 23 before_filter :find_role, :only => [:show, :edit, :update, :destroy]
24 24 accept_api_auth :index, :show
25 25
26 26 def index
27 27 respond_to do |format|
28 28 format.html {
29 29 @role_pages, @roles = paginate Role.sorted, :per_page => 25
30 30 render :action => "index", :layout => false if request.xhr?
31 31 }
32 32 format.api {
33 33 @roles = Role.givable.all
34 34 }
35 35 end
36 36 end
37 37
38 38 def show
39 39 respond_to do |format|
40 40 format.api
41 41 end
42 42 end
43 43
44 44 def new
45 45 # Prefills the form with 'Non member' role permissions by default
46 46 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
47 47 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
48 48 @role.copy_from(@copy_from)
49 49 end
50 50 @roles = Role.sorted.all
51 51 end
52 52
53 53 def create
54 54 @role = Role.new(params[:role])
55 55 if request.post? && @role.save
56 56 # workflow copy
57 57 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
58 58 @role.workflow_rules.copy(copy_from)
59 59 end
60 60 flash[:notice] = l(:notice_successful_create)
61 61 redirect_to roles_path
62 62 else
63 63 @roles = Role.sorted.all
64 64 render :action => 'new'
65 65 end
66 66 end
67 67
68 68 def edit
69 69 end
70 70
71 71 def update
72 72 if request.put? and @role.update_attributes(params[:role])
73 73 flash[:notice] = l(:notice_successful_update)
74 74 redirect_to roles_path
75 75 else
76 76 render :action => 'edit'
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @role.destroy
82 82 redirect_to roles_path
83 83 rescue
84 84 flash[:error] = l(:error_can_not_remove_role)
85 85 redirect_to roles_path
86 86 end
87 87
88 88 def permissions
89 89 @roles = Role.sorted.all
90 90 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
91 91 if request.post?
92 92 @roles.each do |role|
93 93 role.permissions = params[:permissions][role.id.to_s]
94 94 role.save
95 95 end
96 96 flash[:notice] = l(:notice_successful_update)
97 97 redirect_to roles_path
98 98 end
99 99 end
100 100
101 101 private
102 102
103 103 def find_role
104 104 @role = Role.find(params[:id])
105 105 rescue ActiveRecord::RecordNotFound
106 106 render_404
107 107 end
108 108 end
@@ -1,111 +1,111
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 21 def index
22 22 @question = params[:q] || ""
23 23 @question.strip!
24 24 @all_words = params[:all_words] ? params[:all_words].present? : true
25 25 @titles_only = params[:titles_only] ? params[:titles_only].present? : false
26 26
27 27 projects_to_search =
28 28 case params[:scope]
29 29 when 'all'
30 30 nil
31 31 when 'my_projects'
32 32 User.current.memberships.collect(&:project)
33 33 when 'subprojects'
34 34 @project ? (@project.self_and_descendants.active.all) : nil
35 35 else
36 36 @project
37 37 end
38 38
39 39 offset = nil
40 40 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
41 41
42 42 # quick jump to an issue
43 43 if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
44 44 redirect_to issue_path(issue)
45 45 return
46 46 end
47 47
48 48 @object_types = Redmine::Search.available_search_types.dup
49 49 if projects_to_search.is_a? Project
50 50 # don't search projects
51 51 @object_types.delete('projects')
52 52 # only show what the user is allowed to view
53 53 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
54 54 end
55 55
56 56 @scope = @object_types.select {|t| params[t]}
57 57 @scope = @object_types if @scope.empty?
58 58
59 59 # extract tokens from the question
60 60 # eg. hello "bye bye" => ["hello", "bye bye"]
61 61 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
62 62 # tokens must be at least 2 characters long
63 63 @tokens = @tokens.uniq.select {|w| w.length > 1 }
64 64
65 65 if !@tokens.empty?
66 66 # no more than 5 tokens to search for
67 67 @tokens.slice! 5..-1 if @tokens.size > 5
68 68
69 69 @results = []
70 70 @results_by_type = Hash.new {|h,k| h[k] = 0}
71 71
72 72 limit = 10
73 73 @scope.each do |s|
74 74 r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
75 75 :all_words => @all_words,
76 76 :titles_only => @titles_only,
77 77 :limit => (limit+1),
78 78 :offset => offset,
79 79 :before => params[:previous].nil?)
80 80 @results += r
81 81 @results_by_type[s] += c
82 82 end
83 83 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
84 84 if params[:previous].nil?
85 85 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
86 86 if @results.size > limit
87 87 @pagination_next_date = @results[limit-1].event_datetime
88 88 @results = @results[0, limit]
89 89 end
90 90 else
91 91 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
92 92 if @results.size > limit
93 93 @pagination_previous_date = @results[-(limit)].event_datetime
94 94 @results = @results[-(limit), limit]
95 95 end
96 96 end
97 97 else
98 98 @question = ""
99 99 end
100 100 render :layout => false if request.xhr?
101 101 end
102 102
103 103 private
104 104 def find_optional_project
105 105 return true unless params[:id]
106 106 @project = Project.find(params[:id])
107 107 check_project_privacy
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111 end
@@ -1,66 +1,66
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 before_filter :require_admin
23 23
24 24 def index
25 25 edit
26 26 render :action => 'edit'
27 27 end
28 28
29 29 def edit
30 30 @notifiables = Redmine::Notifiable.all
31 31 if request.post? && params[:settings] && params[:settings].is_a?(Hash)
32 32 settings = (params[:settings] || {}).dup.symbolize_keys
33 33 settings.each do |name, value|
34 34 # remove blank values in array settings
35 35 value.delete_if {|v| v.blank? } if value.is_a?(Array)
36 36 Setting[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 Redmine::Themes.rescan
50 50 end
51 51 end
52 52
53 53 def plugin
54 54 @plugin = Redmine::Plugin.find(params[:id])
55 55 if request.post?
56 56 Setting.send "plugin_#{@plugin.id}=", params[:settings]
57 57 flash[:notice] = l(:notice_successful_update)
58 58 redirect_to plugin_settings_path(@plugin)
59 59 else
60 60 @partial = @plugin.settings[:partial]
61 61 @settings = Setting.send "plugin_#{@plugin.id}"
62 62 end
63 63 rescue Redmine::PluginNotFound
64 64 render_404
65 65 end
66 66 end
@@ -1,84 +1,84
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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).find(
23 23 :all,
24 24 :include => :repository,
25 25 :order => "#{Project.table_name}.identifier"
26 26 )
27 27 # extra_info attribute from repository breaks activeresource client
28 28 render :xml => p.to_xml(
29 29 :only => [:id, :identifier, :name, :is_public, :status],
30 30 :include => {:repository => {:only => [:id, :url]}}
31 31 )
32 32 end
33 33
34 34 def create_project_repository
35 35 project = Project.find(params[:id])
36 36 if project.repository
37 37 render :nothing => true, :status => 409
38 38 else
39 39 logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
40 40 repository = Repository.factory(params[:vendor], params[:repository])
41 41 repository.project = project
42 42 if repository.save
43 43 render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
44 44 else
45 45 render :nothing => true, :status => 422
46 46 end
47 47 end
48 48 end
49 49
50 50 def fetch_changesets
51 51 projects = []
52 52 scope = Project.active.has_module(:repository)
53 53 if params[:id]
54 54 project = nil
55 55 if params[:id].to_s =~ /^\d*$/
56 56 project = scope.find(params[:id])
57 57 else
58 58 project = scope.find_by_identifier(params[:id])
59 59 end
60 60 raise ActiveRecord::RecordNotFound unless project
61 61 projects << project
62 62 else
63 63 projects = scope.all
64 64 end
65 65 projects.each do |project|
66 66 project.repositories.each do |repository|
67 67 repository.fetch_changesets
68 68 end
69 69 end
70 70 render :nothing => true, :status => 200
71 71 rescue ActiveRecord::RecordNotFound
72 72 render :nothing => true, :status => 404
73 73 end
74 74
75 75 protected
76 76
77 77 def check_enabled
78 78 User.current = nil
79 79 unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
80 80 render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
81 81 return false
82 82 end
83 83 end
84 84 end
@@ -1,313 +1,313
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 20
21 21 before_filter :find_project_for_new_time_entry, :only => [:create]
22 22 before_filter :find_time_entry, :only => [:show, :edit, :update]
23 23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 24 before_filter :authorize, :except => [:new, :index, :report]
25 25
26 26 before_filter :find_optional_project, :only => [:index, :report]
27 27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
28 28 before_filter :authorize_global, :only => [:new, :index, :report]
29 29
30 30 accept_rss_auth :index
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :sort
36 36 include SortHelper
37 37 helper :issues
38 38 include TimelogHelper
39 39 helper :custom_fields
40 40 include CustomFieldsHelper
41 41 helper :queries
42 42
43 43 def index
44 44 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
45 45 scope = time_entry_scope
46 46
47 47 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
48 48 sort_update(@query.sortable_columns)
49 49
50 50 respond_to do |format|
51 51 format.html {
52 52 # Paginate results
53 53 @entry_count = scope.count
54 54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 55 @entries = scope.all(
56 56 :include => [:project, :activity, :user, {:issue => :tracker}],
57 57 :order => sort_clause,
58 58 :limit => @entry_pages.items_per_page,
59 59 :offset => @entry_pages.offset
60 60 )
61 61 @total_hours = scope.sum(:hours).to_f
62 62
63 63 render :layout => !request.xhr?
64 64 }
65 65 format.api {
66 66 @entry_count = scope.count
67 67 @offset, @limit = api_offset_and_limit
68 68 @entries = scope.all(
69 69 :include => [:project, :activity, :user, {:issue => :tracker}],
70 70 :order => sort_clause,
71 71 :limit => @limit,
72 72 :offset => @offset
73 73 )
74 74 }
75 75 format.atom {
76 76 entries = scope.all(
77 77 :include => [:project, :activity, :user, {:issue => :tracker}],
78 78 :order => "#{TimeEntry.table_name}.created_on DESC",
79 79 :limit => Setting.feeds_limit.to_i
80 80 )
81 81 render_feed(entries, :title => l(:label_spent_time))
82 82 }
83 83 format.csv {
84 84 # Export all entries
85 85 @entries = scope.all(
86 86 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
87 87 :order => sort_clause
88 88 )
89 89 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
90 90 }
91 91 end
92 92 end
93 93
94 94 def report
95 95 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
96 96 scope = time_entry_scope
97 97
98 98 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
99 99
100 100 respond_to do |format|
101 101 format.html { render :layout => !request.xhr? }
102 102 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
103 103 end
104 104 end
105 105
106 106 def show
107 107 respond_to do |format|
108 108 # TODO: Implement html response
109 109 format.html { render :nothing => true, :status => 406 }
110 110 format.api
111 111 end
112 112 end
113 113
114 114 def new
115 115 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
116 116 @time_entry.safe_attributes = params[:time_entry]
117 117 end
118 118
119 119 def create
120 120 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
121 121 @time_entry.safe_attributes = params[:time_entry]
122 122
123 123 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
124 124
125 125 if @time_entry.save
126 126 respond_to do |format|
127 127 format.html {
128 128 flash[:notice] = l(:notice_successful_create)
129 129 if params[:continue]
130 130 if params[:project_id]
131 131 options = {
132 132 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
133 133 :back_url => params[:back_url]
134 134 }
135 135 if @time_entry.issue
136 136 redirect_to new_project_issue_time_entry_path(@time_entry.project, @time_entry.issue, options)
137 137 else
138 138 redirect_to new_project_time_entry_path(@time_entry.project, options)
139 139 end
140 140 else
141 141 options = {
142 142 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
143 143 :back_url => params[:back_url]
144 144 }
145 145 redirect_to new_time_entry_path(options)
146 146 end
147 147 else
148 148 redirect_back_or_default project_time_entries_path(@time_entry.project)
149 149 end
150 150 }
151 151 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
152 152 end
153 153 else
154 154 respond_to do |format|
155 155 format.html { render :action => 'new' }
156 156 format.api { render_validation_errors(@time_entry) }
157 157 end
158 158 end
159 159 end
160 160
161 161 def edit
162 162 @time_entry.safe_attributes = params[:time_entry]
163 163 end
164 164
165 165 def update
166 166 @time_entry.safe_attributes = params[:time_entry]
167 167
168 168 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
169 169
170 170 if @time_entry.save
171 171 respond_to do |format|
172 172 format.html {
173 173 flash[:notice] = l(:notice_successful_update)
174 174 redirect_back_or_default project_time_entries_path(@time_entry.project)
175 175 }
176 176 format.api { render_api_ok }
177 177 end
178 178 else
179 179 respond_to do |format|
180 180 format.html { render :action => 'edit' }
181 181 format.api { render_validation_errors(@time_entry) }
182 182 end
183 183 end
184 184 end
185 185
186 186 def bulk_edit
187 187 @available_activities = TimeEntryActivity.shared.active
188 188 @custom_fields = TimeEntry.first.available_custom_fields
189 189 end
190 190
191 191 def bulk_update
192 192 attributes = parse_params_for_bulk_time_entry_attributes(params)
193 193
194 194 unsaved_time_entry_ids = []
195 195 @time_entries.each do |time_entry|
196 196 time_entry.reload
197 197 time_entry.safe_attributes = attributes
198 198 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
199 199 unless time_entry.save
200 200 # Keep unsaved time_entry ids to display them in flash error
201 201 unsaved_time_entry_ids << time_entry.id
202 202 end
203 203 end
204 204 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
205 205 redirect_back_or_default project_time_entries_path(@projects.first)
206 206 end
207 207
208 208 def destroy
209 209 destroyed = TimeEntry.transaction do
210 210 @time_entries.each do |t|
211 211 unless t.destroy && t.destroyed?
212 212 raise ActiveRecord::Rollback
213 213 end
214 214 end
215 215 end
216 216
217 217 respond_to do |format|
218 218 format.html {
219 219 if destroyed
220 220 flash[:notice] = l(:notice_successful_delete)
221 221 else
222 222 flash[:error] = l(:notice_unable_delete_time_entry)
223 223 end
224 224 redirect_back_or_default project_time_entries_path(@projects.first)
225 225 }
226 226 format.api {
227 227 if destroyed
228 228 render_api_ok
229 229 else
230 230 render_validation_errors(@time_entries)
231 231 end
232 232 }
233 233 end
234 234 end
235 235
236 236 private
237 237 def find_time_entry
238 238 @time_entry = TimeEntry.find(params[:id])
239 239 unless @time_entry.editable_by?(User.current)
240 240 render_403
241 241 return false
242 242 end
243 243 @project = @time_entry.project
244 244 rescue ActiveRecord::RecordNotFound
245 245 render_404
246 246 end
247 247
248 248 def find_time_entries
249 249 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
250 250 raise ActiveRecord::RecordNotFound if @time_entries.empty?
251 251 @projects = @time_entries.collect(&:project).compact.uniq
252 252 @project = @projects.first if @projects.size == 1
253 253 rescue ActiveRecord::RecordNotFound
254 254 render_404
255 255 end
256 256
257 257 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
258 258 if unsaved_time_entry_ids.empty?
259 259 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
260 260 else
261 261 flash[:error] = l(:notice_failed_to_save_time_entries,
262 262 :count => unsaved_time_entry_ids.size,
263 263 :total => time_entries.size,
264 264 :ids => '#' + unsaved_time_entry_ids.join(', #'))
265 265 end
266 266 end
267 267
268 268 def find_optional_project_for_new_time_entry
269 269 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
270 270 @project = Project.find(project_id)
271 271 end
272 272 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
273 273 @issue = Issue.find(issue_id)
274 274 @project ||= @issue.project
275 275 end
276 276 rescue ActiveRecord::RecordNotFound
277 277 render_404
278 278 end
279 279
280 280 def find_project_for_new_time_entry
281 281 find_optional_project_for_new_time_entry
282 282 if @project.nil?
283 283 render_404
284 284 end
285 285 end
286 286
287 287 def find_optional_project
288 288 if !params[:issue_id].blank?
289 289 @issue = Issue.find(params[:issue_id])
290 290 @project = @issue.project
291 291 elsif !params[:project_id].blank?
292 292 @project = Project.find(params[:project_id])
293 293 end
294 294 end
295 295
296 296 # Returns the TimeEntry scope for index and report actions
297 297 def time_entry_scope
298 298 scope = TimeEntry.visible.where(@query.statement)
299 299 if @issue
300 300 scope = scope.on_issue(@issue)
301 301 elsif @project
302 302 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
303 303 end
304 304 scope
305 305 end
306 306
307 307 def parse_params_for_bulk_time_entry_attributes(params)
308 308 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
309 309 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
310 310 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
311 311 attributes
312 312 end
313 313 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TrackersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @tracker_pages, @trackers = paginate Tracker.sorted, :per_page => 25
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @trackers = Tracker.sorted.all
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @tracker ||= Tracker.new(params[:tracker])
39 39 @trackers = Tracker.sorted.all
40 40 @projects = Project.all
41 41 end
42 42
43 43 def create
44 44 @tracker = Tracker.new(params[:tracker])
45 45 if @tracker.save
46 46 # workflow copy
47 47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 48 @tracker.workflow_rules.copy(copy_from)
49 49 end
50 50 flash[:notice] = l(:notice_successful_create)
51 51 redirect_to trackers_path
52 52 return
53 53 end
54 54 new
55 55 render :action => 'new'
56 56 end
57 57
58 58 def edit
59 59 @tracker ||= Tracker.find(params[:id])
60 60 @projects = Project.all
61 61 end
62 62
63 63 def update
64 64 @tracker = Tracker.find(params[:id])
65 65 if @tracker.update_attributes(params[:tracker])
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to trackers_path
68 68 return
69 69 end
70 70 edit
71 71 render :action => 'edit'
72 72 end
73 73
74 74 def destroy
75 75 @tracker = Tracker.find(params[:id])
76 76 unless @tracker.issues.empty?
77 77 flash[:error] = l(:error_can_not_delete_tracker)
78 78 else
79 79 @tracker.destroy
80 80 end
81 81 redirect_to trackers_path
82 82 end
83 83
84 84 def fields
85 85 if request.post? && params[:trackers]
86 86 params[:trackers].each do |tracker_id, tracker_params|
87 87 tracker = Tracker.find_by_id(tracker_id)
88 88 if tracker
89 89 tracker.core_fields = tracker_params[:core_fields]
90 90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
91 91 tracker.save
92 92 end
93 93 end
94 94 flash[:notice] = l(:notice_successful_update)
95 95 redirect_to fields_trackers_path
96 96 return
97 97 end
98 98 @trackers = Tracker.sorted.all
99 99 @custom_fields = IssueCustomField.all.sort
100 100 end
101 101 end
@@ -1,212 +1,212
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
23 23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29
30 30 def index
31 31 sort_init 'login', 'asc'
32 32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33 33
34 34 case params[:format]
35 35 when 'xml', 'json'
36 36 @offset, @limit = api_offset_and_limit
37 37 else
38 38 @limit = per_page_option
39 39 end
40 40
41 41 @status = params[:status] || 1
42 42
43 43 scope = User.logged.status(@status)
44 44 scope = scope.like(params[:name]) if params[:name].present?
45 45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
46 46
47 47 @user_count = scope.count
48 48 @user_pages = Paginator.new @user_count, @limit, params['page']
49 49 @offset ||= @user_pages.offset
50 50 @users = scope.order(sort_clause).limit(@limit).offset(@offset).all
51 51
52 52 respond_to do |format|
53 53 format.html {
54 54 @groups = Group.all.sort
55 55 render :layout => !request.xhr?
56 56 }
57 57 format.api
58 58 end
59 59 end
60 60
61 61 def show
62 62 # show projects based on current user visibility
63 63 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
64 64
65 65 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
66 66 @events_by_day = events.group_by(&:event_date)
67 67
68 68 unless User.current.admin?
69 69 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
70 70 render_404
71 71 return
72 72 end
73 73 end
74 74
75 75 respond_to do |format|
76 76 format.html { render :layout => 'base' }
77 77 format.api
78 78 end
79 79 end
80 80
81 81 def new
82 82 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
83 83 @auth_sources = AuthSource.all
84 84 end
85 85
86 86 def create
87 87 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
88 88 @user.safe_attributes = params[:user]
89 89 @user.admin = params[:user][:admin] || false
90 90 @user.login = params[:user][:login]
91 91 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
92 92
93 93 if @user.save
94 94 @user.pref.attributes = params[:pref]
95 95 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
96 96 @user.pref.save
97 97 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
98 98
99 99 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
100 100
101 101 respond_to do |format|
102 102 format.html {
103 103 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
104 104 if params[:continue]
105 105 redirect_to new_user_path
106 106 else
107 107 redirect_to edit_user_path(@user)
108 108 end
109 109 }
110 110 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
111 111 end
112 112 else
113 113 @auth_sources = AuthSource.all
114 114 # Clear password input
115 115 @user.password = @user.password_confirmation = nil
116 116
117 117 respond_to do |format|
118 118 format.html { render :action => 'new' }
119 119 format.api { render_validation_errors(@user) }
120 120 end
121 121 end
122 122 end
123 123
124 124 def edit
125 125 @auth_sources = AuthSource.all
126 126 @membership ||= Member.new
127 127 end
128 128
129 129 def update
130 130 @user.admin = params[:user][:admin] if params[:user][:admin]
131 131 @user.login = params[:user][:login] if params[:user][:login]
132 132 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
133 133 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
134 134 end
135 135 @user.safe_attributes = params[:user]
136 136 # Was the account actived ? (do it before User#save clears the change)
137 137 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
138 138 # TODO: Similar to My#account
139 139 @user.pref.attributes = params[:pref]
140 140 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
141 141
142 142 if @user.save
143 143 @user.pref.save
144 144 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
145 145
146 146 if was_activated
147 147 Mailer.account_activated(@user).deliver
148 148 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
149 149 Mailer.account_information(@user, params[:user][:password]).deliver
150 150 end
151 151
152 152 respond_to do |format|
153 153 format.html {
154 154 flash[:notice] = l(:notice_successful_update)
155 155 redirect_to_referer_or edit_user_path(@user)
156 156 }
157 157 format.api { render_api_ok }
158 158 end
159 159 else
160 160 @auth_sources = AuthSource.all
161 161 @membership ||= Member.new
162 162 # Clear password input
163 163 @user.password = @user.password_confirmation = nil
164 164
165 165 respond_to do |format|
166 166 format.html { render :action => :edit }
167 167 format.api { render_validation_errors(@user) }
168 168 end
169 169 end
170 170 end
171 171
172 172 def destroy
173 173 @user.destroy
174 174 respond_to do |format|
175 175 format.html { redirect_back_or_default(users_path) }
176 176 format.api { render_api_ok }
177 177 end
178 178 end
179 179
180 180 def edit_membership
181 181 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
182 182 @membership.save
183 183 respond_to do |format|
184 184 format.html { redirect_to edit_user_path(@user, :tab => 'memberships') }
185 185 format.js
186 186 end
187 187 end
188 188
189 189 def destroy_membership
190 190 @membership = Member.find(params[:membership_id])
191 191 if @membership.deletable?
192 192 @membership.destroy
193 193 end
194 194 respond_to do |format|
195 195 format.html { redirect_to edit_user_path(@user, :tab => 'memberships') }
196 196 format.js
197 197 end
198 198 end
199 199
200 200 private
201 201
202 202 def find_user
203 203 if params[:id] == 'current'
204 204 require_login || return
205 205 @user = User.current
206 206 else
207 207 @user = User.find(params[:id])
208 208 end
209 209 rescue ActiveRecord::RecordNotFound
210 210 render_404
211 211 end
212 212 end
@@ -1,182 +1,182
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 model_object Version
21 21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 23 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
24 24 before_filter :authorize
25 25
26 26 accept_api_auth :index, :show, :create, :update, :destroy
27 27
28 28 helper :custom_fields
29 29 helper :projects
30 30
31 31 def index
32 32 respond_to do |format|
33 33 format.html {
34 34 @trackers = @project.trackers.sorted.all
35 35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38 38
39 39 @versions = @project.shared_versions || []
40 40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 41 @versions = @versions.uniq.sort
42 42 unless params[:completed]
43 43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 44 @versions -= @completed_versions
45 45 end
46 46
47 47 @issues_by_version = {}
48 48 if @selected_tracker_ids.any? && @versions.any?
49 49 issues = Issue.visible.all(
50 50 :include => [:project, :status, :tracker, :priority, :fixed_version],
51 51 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
52 52 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
53 53 )
54 54 @issues_by_version = issues.group_by(&:fixed_version)
55 55 end
56 56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 57 }
58 58 format.api {
59 59 @versions = @project.shared_versions.all
60 60 }
61 61 end
62 62 end
63 63
64 64 def show
65 65 respond_to do |format|
66 66 format.html {
67 67 @issues = @version.fixed_issues.visible.
68 68 includes(:status, :tracker, :priority).
69 69 reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
70 70 all
71 71 }
72 72 format.api
73 73 end
74 74 end
75 75
76 76 def new
77 77 @version = @project.versions.build
78 78 @version.safe_attributes = params[:version]
79 79
80 80 respond_to do |format|
81 81 format.html
82 82 format.js
83 83 end
84 84 end
85 85
86 86 def create
87 87 @version = @project.versions.build
88 88 if params[:version]
89 89 attributes = params[:version].dup
90 90 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
91 91 @version.safe_attributes = attributes
92 92 end
93 93
94 94 if request.post?
95 95 if @version.save
96 96 respond_to do |format|
97 97 format.html do
98 98 flash[:notice] = l(:notice_successful_create)
99 99 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
100 100 end
101 101 format.js
102 102 format.api do
103 103 render :action => 'show', :status => :created, :location => version_url(@version)
104 104 end
105 105 end
106 106 else
107 107 respond_to do |format|
108 108 format.html { render :action => 'new' }
109 109 format.js { render :action => 'new' }
110 110 format.api { render_validation_errors(@version) }
111 111 end
112 112 end
113 113 end
114 114 end
115 115
116 116 def edit
117 117 end
118 118
119 119 def update
120 120 if request.put? && params[:version]
121 121 attributes = params[:version].dup
122 122 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
123 123 @version.safe_attributes = attributes
124 124 if @version.save
125 125 respond_to do |format|
126 126 format.html {
127 127 flash[:notice] = l(:notice_successful_update)
128 128 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
129 129 }
130 130 format.api { render_api_ok }
131 131 end
132 132 else
133 133 respond_to do |format|
134 134 format.html { render :action => 'edit' }
135 135 format.api { render_validation_errors(@version) }
136 136 end
137 137 end
138 138 end
139 139 end
140 140
141 141 def close_completed
142 142 if request.put?
143 143 @project.close_completed_versions
144 144 end
145 145 redirect_to settings_project_path(@project, :tab => 'versions')
146 146 end
147 147
148 148 def destroy
149 149 if @version.fixed_issues.empty?
150 150 @version.destroy
151 151 respond_to do |format|
152 152 format.html { redirect_back_or_default settings_project_path(@project, :tab => 'versions') }
153 153 format.api { render_api_ok }
154 154 end
155 155 else
156 156 respond_to do |format|
157 157 format.html {
158 158 flash[:error] = l(:notice_unable_delete_version)
159 159 redirect_to settings_project_path(@project, :tab => 'versions')
160 160 }
161 161 format.api { head :unprocessable_entity }
162 162 end
163 163 end
164 164 end
165 165
166 166 def status_by
167 167 respond_to do |format|
168 168 format.html { render :action => 'show' }
169 169 format.js
170 170 end
171 171 end
172 172
173 173 private
174 174
175 175 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
176 176 if ids = params[:tracker_ids]
177 177 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
178 178 else
179 179 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
180 180 end
181 181 end
182 182 end
@@ -1,95 +1,95
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 :find_project
20 20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 21 before_filter :authorize, :only => [:new, :destroy]
22 22
23 23 def watch
24 24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
25 25 render_403
26 26 else
27 27 set_watcher(User.current, true)
28 28 end
29 29 end
30 30
31 31 def unwatch
32 32 set_watcher(User.current, false)
33 33 end
34 34
35 35 def new
36 36 end
37 37
38 38 def create
39 39 if params[:watcher].is_a?(Hash) && request.post?
40 40 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
41 41 user_ids.each do |user_id|
42 42 Watcher.create(:watchable => @watched, :user_id => user_id)
43 43 end
44 44 end
45 45 respond_to do |format|
46 46 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
47 47 format.js
48 48 end
49 49 end
50 50
51 51 def append
52 52 if params[:watcher].is_a?(Hash)
53 53 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
54 54 @users = User.active.find_all_by_id(user_ids)
55 55 end
56 56 end
57 57
58 58 def destroy
59 59 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
60 60 respond_to do |format|
61 61 format.html { redirect_to :back }
62 62 format.js
63 63 end
64 64 end
65 65
66 66 def autocomplete_for_user
67 67 @users = User.active.like(params[:q]).limit(100).all
68 68 if @watched
69 69 @users -= @watched.watcher_users
70 70 end
71 71 render :layout => false
72 72 end
73 73
74 74 private
75 75 def find_project
76 76 if params[:object_type] && params[:object_id]
77 77 klass = Object.const_get(params[:object_type].camelcase)
78 78 return false unless klass.respond_to?('watched_by')
79 79 @watched = klass.find(params[:object_id])
80 80 @project = @watched.project
81 81 elsif params[:project_id]
82 82 @project = Project.visible.find_by_param(params[:project_id])
83 83 end
84 84 rescue
85 85 render_404
86 86 end
87 87
88 88 def set_watcher(user, watching)
89 89 @watched.set_watcher(user, watching)
90 90 respond_to do |format|
91 91 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
92 92 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
93 93 end
94 94 end
95 95 end
@@ -1,30 +1,30
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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,356 +1,356
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 'diff'
19 19
20 20 # The WikiController follows the Rails REST controller pattern but with
21 21 # a few differences
22 22 #
23 23 # * index - shows a list of WikiPages grouped by page or date
24 24 # * new - not used
25 25 # * create - not used
26 26 # * show - will also show the form for creating a new wiki page
27 27 # * edit - used to edit an existing or new page
28 28 # * update - used to save a wiki page update to the database, including new pages
29 29 # * destroy - normal
30 30 #
31 31 # Other member and collection methods are also used
32 32 #
33 33 # TODO: still being worked on
34 34 class WikiController < ApplicationController
35 35 default_search_scope :wiki_pages
36 36 before_filter :find_wiki, :authorize
37 37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39 39 accept_api_auth :index, :show, :update, :destroy
40 40 before_filter :find_attachments, :only => [:preview]
41 41
42 42 helper :attachments
43 43 include AttachmentsHelper
44 44 helper :watchers
45 45 include Redmine::Export::PDF
46 46
47 47 # List of pages, sorted alphabetically and by parent (hierarchy)
48 48 def index
49 49 load_pages_for_index
50 50
51 51 respond_to do |format|
52 52 format.html {
53 53 @pages_by_parent_id = @pages.group_by(&:parent_id)
54 54 }
55 55 format.api
56 56 end
57 57 end
58 58
59 59 # List of page, by last update
60 60 def date_index
61 61 load_pages_for_index
62 62 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
63 63 end
64 64
65 65 # display a page (in editing mode if it doesn't exist)
66 66 def show
67 67 if @page.new_record?
68 68 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
69 69 edit
70 70 render :action => 'edit'
71 71 else
72 72 render_404
73 73 end
74 74 return
75 75 end
76 76 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
77 77 deny_access
78 78 return
79 79 end
80 80 @content = @page.content_for_version(params[:version])
81 81 if User.current.allowed_to?(:export_wiki_pages, @project)
82 82 if params[:format] == 'pdf'
83 83 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
84 84 return
85 85 elsif params[:format] == 'html'
86 86 export = render_to_string :action => 'export', :layout => false
87 87 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
88 88 return
89 89 elsif params[:format] == 'txt'
90 90 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
91 91 return
92 92 end
93 93 end
94 94 @editable = editable?
95 95 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
96 96 @content.current_version? &&
97 97 Redmine::WikiFormatting.supports_section_edit?
98 98
99 99 respond_to do |format|
100 100 format.html
101 101 format.api
102 102 end
103 103 end
104 104
105 105 # edit an existing page or a new one
106 106 def edit
107 107 return render_403 unless editable?
108 108 if @page.new_record?
109 109 @page.content = WikiContent.new(:page => @page)
110 110 if params[:parent].present?
111 111 @page.parent = @page.wiki.find_page(params[:parent].to_s)
112 112 end
113 113 end
114 114
115 115 @content = @page.content_for_version(params[:version])
116 116 @content.text = initial_page_content(@page) if @content.text.blank?
117 117 # don't keep previous comment
118 118 @content.comments = nil
119 119
120 120 # To prevent StaleObjectError exception when reverting to a previous version
121 121 @content.version = @page.content.version
122 122
123 123 @text = @content.text
124 124 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
125 125 @section = params[:section].to_i
126 126 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
127 127 render_404 if @text.blank?
128 128 end
129 129 end
130 130
131 131 # Creates a new page or updates an existing one
132 132 def update
133 133 return render_403 unless editable?
134 134 was_new_page = @page.new_record?
135 135 @page.content = WikiContent.new(:page => @page) if @page.new_record?
136 136 @page.safe_attributes = params[:wiki_page]
137 137
138 138 @content = @page.content
139 139 content_params = params[:content]
140 140 if content_params.nil? && params[:wiki_page].is_a?(Hash)
141 141 content_params = params[:wiki_page].slice(:text, :comments, :version)
142 142 end
143 143 content_params ||= {}
144 144
145 145 @content.comments = content_params[:comments]
146 146 @text = content_params[:text]
147 147 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
148 148 @section = params[:section].to_i
149 149 @section_hash = params[:section_hash]
150 150 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
151 151 else
152 152 @content.version = content_params[:version] if content_params[:version]
153 153 @content.text = @text
154 154 end
155 155 @content.author = User.current
156 156
157 157 if @page.save_with_content
158 158 attachments = Attachment.attach_files(@page, params[:attachments])
159 159 render_attachment_warning_if_needed(@page)
160 160 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
161 161
162 162 respond_to do |format|
163 163 format.html { redirect_to project_wiki_page_path(@project, @page.title) }
164 164 format.api {
165 165 if was_new_page
166 166 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
167 167 else
168 168 render_api_ok
169 169 end
170 170 }
171 171 end
172 172 else
173 173 respond_to do |format|
174 174 format.html { render :action => 'edit' }
175 175 format.api { render_validation_errors(@content) }
176 176 end
177 177 end
178 178
179 179 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
180 180 # Optimistic locking exception
181 181 respond_to do |format|
182 182 format.html {
183 183 flash.now[:error] = l(:notice_locking_conflict)
184 184 render :action => 'edit'
185 185 }
186 186 format.api { render_api_head :conflict }
187 187 end
188 188 rescue ActiveRecord::RecordNotSaved
189 189 respond_to do |format|
190 190 format.html { render :action => 'edit' }
191 191 format.api { render_validation_errors(@content) }
192 192 end
193 193 end
194 194
195 195 # rename a page
196 196 def rename
197 197 return render_403 unless editable?
198 198 @page.redirect_existing_links = true
199 199 # used to display the *original* title if some AR validation errors occur
200 200 @original_title = @page.pretty_title
201 201 if request.post? && @page.update_attributes(params[:wiki_page])
202 202 flash[:notice] = l(:notice_successful_update)
203 203 redirect_to project_wiki_page_path(@project, @page.title)
204 204 end
205 205 end
206 206
207 207 def protect
208 208 @page.update_attribute :protected, params[:protected]
209 209 redirect_to project_wiki_page_path(@project, @page.title)
210 210 end
211 211
212 212 # show page history
213 213 def history
214 214 @version_count = @page.content.versions.count
215 215 @version_pages = Paginator.new @version_count, per_page_option, params['page']
216 216 # don't load text
217 217 @versions = @page.content.versions.
218 218 select("id, author_id, comments, updated_on, version").
219 219 reorder('version DESC').
220 220 limit(@version_pages.items_per_page + 1).
221 221 offset(@version_pages.offset).
222 222 all
223 223
224 224 render :layout => false if request.xhr?
225 225 end
226 226
227 227 def diff
228 228 @diff = @page.diff(params[:version], params[:version_from])
229 229 render_404 unless @diff
230 230 end
231 231
232 232 def annotate
233 233 @annotate = @page.annotate(params[:version])
234 234 render_404 unless @annotate
235 235 end
236 236
237 237 # Removes a wiki page and its history
238 238 # Children can be either set as root pages, removed or reassigned to another parent page
239 239 def destroy
240 240 return render_403 unless editable?
241 241
242 242 @descendants_count = @page.descendants.size
243 243 if @descendants_count > 0
244 244 case params[:todo]
245 245 when 'nullify'
246 246 # Nothing to do
247 247 when 'destroy'
248 248 # Removes all its descendants
249 249 @page.descendants.each(&:destroy)
250 250 when 'reassign'
251 251 # Reassign children to another parent page
252 252 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
253 253 return unless reassign_to
254 254 @page.children.each do |child|
255 255 child.update_attribute(:parent, reassign_to)
256 256 end
257 257 else
258 258 @reassignable_to = @wiki.pages - @page.self_and_descendants
259 259 # display the destroy form if it's a user request
260 260 return unless api_request?
261 261 end
262 262 end
263 263 @page.destroy
264 264 respond_to do |format|
265 265 format.html { redirect_to project_wiki_index_path(@project) }
266 266 format.api { render_api_ok }
267 267 end
268 268 end
269 269
270 270 def destroy_version
271 271 return render_403 unless editable?
272 272
273 273 @content = @page.content_for_version(params[:version])
274 274 @content.destroy
275 275 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
276 276 end
277 277
278 278 # Export wiki to a single pdf or html file
279 279 def export
280 280 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
281 281 respond_to do |format|
282 282 format.html {
283 283 export = render_to_string :action => 'export_multiple', :layout => false
284 284 send_data(export, :type => 'text/html', :filename => "wiki.html")
285 285 }
286 286 format.pdf {
287 287 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
288 288 }
289 289 end
290 290 end
291 291
292 292 def preview
293 293 page = @wiki.find_page(params[:id])
294 294 # page is nil when previewing a new page
295 295 return render_403 unless page.nil? || editable?(page)
296 296 if page
297 297 @attachments += page.attachments
298 298 @previewed = page.content
299 299 end
300 300 @text = params[:content][:text]
301 301 render :partial => 'common/preview'
302 302 end
303 303
304 304 def add_attachment
305 305 return render_403 unless editable?
306 306 attachments = Attachment.attach_files(@page, params[:attachments])
307 307 render_attachment_warning_if_needed(@page)
308 308 redirect_to :action => 'show', :id => @page.title, :project_id => @project
309 309 end
310 310
311 311 private
312 312
313 313 def find_wiki
314 314 @project = Project.find(params[:project_id])
315 315 @wiki = @project.wiki
316 316 render_404 unless @wiki
317 317 rescue ActiveRecord::RecordNotFound
318 318 render_404
319 319 end
320 320
321 321 # Finds the requested page or a new page if it doesn't exist
322 322 def find_existing_or_new_page
323 323 @page = @wiki.find_or_new_page(params[:id])
324 324 if @wiki.page_found_with_redirect?
325 325 redirect_to params.update(:id => @page.title)
326 326 end
327 327 end
328 328
329 329 # Finds the requested page and returns a 404 error if it doesn't exist
330 330 def find_existing_page
331 331 @page = @wiki.find_page(params[:id])
332 332 if @page.nil?
333 333 render_404
334 334 return
335 335 end
336 336 if @wiki.page_found_with_redirect?
337 337 redirect_to params.update(:id => @page.title)
338 338 end
339 339 end
340 340
341 341 # Returns true if the current user is allowed to edit the page, otherwise false
342 342 def editable?(page = @page)
343 343 page.editable_by?(User.current)
344 344 end
345 345
346 346 # Returns the default content of a new wiki page
347 347 def initial_page_content(page)
348 348 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
349 349 extend helper unless self.instance_of?(helper)
350 350 helper.instance_method(:initial_page_content).bind(self).call(page)
351 351 end
352 352
353 353 def load_pages_for_index
354 354 @pages = @wiki.pages.with_updated_on.reorder("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
355 355 end
356 356 end
@@ -1,36 +1,36
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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,128 +1,128
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :find_roles, :find_trackers
22 22
23 23 def index
24 24 @workflow_counts = WorkflowTransition.count_by_tracker_and_role
25 25 end
26 26
27 27 def edit
28 28 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
29 29 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
30 30
31 31 if request.post?
32 32 WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
33 33 (params[:issue_status] || []).each { |status_id, transitions|
34 34 transitions.each { |new_status_id, options|
35 35 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
36 36 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
37 37 WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 38 }
39 39 }
40 40 if @role.save
41 41 redirect_to workflows_edit_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
42 42 return
43 43 end
44 44 end
45 45
46 46 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
47 47 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
48 48 @statuses = @tracker.issue_statuses
49 49 end
50 50 @statuses ||= IssueStatus.sorted.all
51 51
52 52 if @tracker && @role && @statuses.any?
53 53 workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all
54 54 @workflows = {}
55 55 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
56 56 @workflows['author'] = workflows.select {|w| w.author}
57 57 @workflows['assignee'] = workflows.select {|w| w.assignee}
58 58 end
59 59 end
60 60
61 61 def permissions
62 62 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
63 63 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
64 64
65 65 if request.post? && @role && @tracker
66 66 WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {})
67 67 redirect_to workflows_permissions_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
68 68 return
69 69 end
70 70
71 71 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
72 72 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
73 73 @statuses = @tracker.issue_statuses
74 74 end
75 75 @statuses ||= IssueStatus.sorted.all
76 76
77 77 if @role && @tracker
78 78 @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
79 79 @custom_fields = @tracker.custom_fields
80 80
81 81 @permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w|
82 82 h[w.old_status_id] ||= {}
83 83 h[w.old_status_id][w.field_name] = w.rule
84 84 h
85 85 end
86 86 @statuses.each {|status| @permissions[status.id] ||= {}}
87 87 end
88 88 end
89 89
90 90 def copy
91 91
92 92 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
93 93 @source_tracker = nil
94 94 else
95 95 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
96 96 end
97 97 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
98 98 @source_role = nil
99 99 else
100 100 @source_role = Role.find_by_id(params[:source_role_id].to_i)
101 101 end
102 102
103 103 @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
104 104 @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
105 105
106 106 if request.post?
107 107 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
108 108 flash.now[:error] = l(:error_workflow_copy_source)
109 109 elsif @target_trackers.blank? || @target_roles.blank?
110 110 flash.now[:error] = l(:error_workflow_copy_target)
111 111 else
112 112 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
113 113 flash[:notice] = l(:notice_successful_update)
114 114 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
115 115 end
116 116 end
117 117 end
118 118
119 119 private
120 120
121 121 def find_roles
122 122 @roles = Role.sorted.all
123 123 end
124 124
125 125 def find_trackers
126 126 @trackers = Tracker.sorted.all
127 127 end
128 128 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ActivitiesHelper
21 21 def sort_activity_events(events)
22 22 events_by_group = events.group_by(&:event_group)
23 23 sorted_events = []
24 24 events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event|
25 25 if group_events = events_by_group.delete(event.event_group)
26 26 group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i|
27 27 sorted_events << [e, i > 0]
28 28 end
29 29 end
30 30 end
31 31 sorted_events
32 32 end
33 33 end
@@ -1,27 +1,27
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AdminHelper
21 21 def project_status_options_for_select(selected)
22 22 options_for_select([[l(:label_all), ''],
23 23 [l(:project_status_active), '1'],
24 24 [l(:project_status_closed), '5'],
25 25 [l(:project_status_archived), '9']], selected.to_s)
26 26 end
27 27 end
@@ -1,1234 +1,1234
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = truncate(issue.subject, :length => 60)
76 76 else
77 77 subject = issue.subject
78 78 if options[:truncate]
79 79 subject = truncate(subject, :length => options[:truncate])
80 80 end
81 81 end
82 82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
83 83 s << h(": #{subject}") if subject
84 84 s = h("#{issue.project} - ") + s if options[:project]
85 85 s
86 86 end
87 87
88 88 # Generates a link to an attachment.
89 89 # Options:
90 90 # * :text - Link text (default to attachment filename)
91 91 # * :download - Force download (default: false)
92 92 def link_to_attachment(attachment, options={})
93 93 text = options.delete(:text) || attachment.filename
94 94 action = options.delete(:download) ? 'download' : 'show'
95 95 opt_only_path = {}
96 96 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97 97 options.delete(:only_path)
98 98 link_to(h(text),
99 99 {:controller => 'attachments', :action => action,
100 100 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101 101 options)
102 102 end
103 103
104 104 # Generates a link to a SCM revision
105 105 # Options:
106 106 # * :text - Link text (default to the formatted revision)
107 107 def link_to_revision(revision, repository, options={})
108 108 if repository.is_a?(Project)
109 109 repository = repository.repository
110 110 end
111 111 text = options.delete(:text) || format_revision(revision)
112 112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 113 link_to(
114 114 h(text),
115 115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 116 :title => l(:label_revision_id, format_revision(revision))
117 117 )
118 118 end
119 119
120 120 # Generates a link to a message
121 121 def link_to_message(message, options={}, html_options = nil)
122 122 link_to(
123 123 h(truncate(message.subject, :length => 60)),
124 124 { :controller => 'messages', :action => 'show',
125 125 :board_id => message.board_id,
126 126 :id => (message.parent_id || message.id),
127 127 :r => (message.parent_id && message.id),
128 128 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
129 129 }.merge(options),
130 130 html_options
131 131 )
132 132 end
133 133
134 134 # Generates a link to a project if active
135 135 # Examples:
136 136 #
137 137 # link_to_project(project) # => link to the specified project overview
138 138 # link_to_project(project, :action=>'settings') # => link to project settings
139 139 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 140 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 141 #
142 142 def link_to_project(project, options={}, html_options = nil)
143 143 if project.archived?
144 144 h(project)
145 145 else
146 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 147 link_to(h(project), url, html_options)
148 148 end
149 149 end
150 150
151 151 def wiki_page_path(page, options={})
152 152 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
153 153 end
154 154
155 155 def thumbnail_tag(attachment)
156 156 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
157 157 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
158 158 :title => attachment.filename
159 159 end
160 160
161 161 def toggle_link(name, id, options={})
162 162 onclick = "$('##{id}').toggle(); "
163 163 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
164 164 onclick << "return false;"
165 165 link_to(name, "#", :onclick => onclick)
166 166 end
167 167
168 168 def image_to_function(name, function, html_options = {})
169 169 html_options.symbolize_keys!
170 170 tag(:input, html_options.merge({
171 171 :type => "image", :src => image_path(name),
172 172 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
173 173 }))
174 174 end
175 175
176 176 def format_activity_title(text)
177 177 h(truncate_single_line(text, :length => 100))
178 178 end
179 179
180 180 def format_activity_day(date)
181 181 date == User.current.today ? l(:label_today).titleize : format_date(date)
182 182 end
183 183
184 184 def format_activity_description(text)
185 185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 187 end
188 188
189 189 def format_version_name(version)
190 190 if version.project == @project
191 191 h(version)
192 192 else
193 193 h("#{version.project} - #{version}")
194 194 end
195 195 end
196 196
197 197 def due_date_distance_in_words(date)
198 198 if date
199 199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 200 end
201 201 end
202 202
203 203 # Renders a tree of projects as a nested set of unordered lists
204 204 # The given collection may be a subset of the whole project tree
205 205 # (eg. some intermediate nodes are private and can not be seen)
206 206 def render_project_nested_lists(projects)
207 207 s = ''
208 208 if projects.any?
209 209 ancestors = []
210 210 original_project = @project
211 211 projects.sort_by(&:lft).each do |project|
212 212 # set the project environment to please macros.
213 213 @project = project
214 214 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
215 215 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
216 216 else
217 217 ancestors.pop
218 218 s << "</li>"
219 219 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
220 220 ancestors.pop
221 221 s << "</ul></li>\n"
222 222 end
223 223 end
224 224 classes = (ancestors.empty? ? 'root' : 'child')
225 225 s << "<li class='#{classes}'><div class='#{classes}'>"
226 226 s << h(block_given? ? yield(project) : project.name)
227 227 s << "</div>\n"
228 228 ancestors << project
229 229 end
230 230 s << ("</li></ul>\n" * ancestors.size)
231 231 @project = original_project
232 232 end
233 233 s.html_safe
234 234 end
235 235
236 236 def render_page_hierarchy(pages, node=nil, options={})
237 237 content = ''
238 238 if pages[node]
239 239 content << "<ul class=\"pages-hierarchy\">\n"
240 240 pages[node].each do |page|
241 241 content << "<li>"
242 242 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
243 243 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
244 244 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
245 245 content << "</li>\n"
246 246 end
247 247 content << "</ul>\n"
248 248 end
249 249 content.html_safe
250 250 end
251 251
252 252 # Renders flash messages
253 253 def render_flash_messages
254 254 s = ''
255 255 flash.each do |k,v|
256 256 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
257 257 end
258 258 s.html_safe
259 259 end
260 260
261 261 # Renders tabs and their content
262 262 def render_tabs(tabs)
263 263 if tabs.any?
264 264 render :partial => 'common/tabs', :locals => {:tabs => tabs}
265 265 else
266 266 content_tag 'p', l(:label_no_data), :class => "nodata"
267 267 end
268 268 end
269 269
270 270 # Renders the project quick-jump box
271 271 def render_project_jump_box
272 272 return unless User.current.logged?
273 273 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
274 274 if projects.any?
275 275 options =
276 276 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
277 277 '<option value="" disabled="disabled">---</option>').html_safe
278 278
279 279 options << project_tree_options_for_select(projects, :selected => @project) do |p|
280 280 { :value => project_path(:id => p, :jump => current_menu_item) }
281 281 end
282 282
283 283 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
284 284 end
285 285 end
286 286
287 287 def project_tree_options_for_select(projects, options = {})
288 288 s = ''
289 289 project_tree(projects) do |project, level|
290 290 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
291 291 tag_options = {:value => project.id}
292 292 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
293 293 tag_options[:selected] = 'selected'
294 294 else
295 295 tag_options[:selected] = nil
296 296 end
297 297 tag_options.merge!(yield(project)) if block_given?
298 298 s << content_tag('option', name_prefix + h(project), tag_options)
299 299 end
300 300 s.html_safe
301 301 end
302 302
303 303 # Yields the given block for each project with its level in the tree
304 304 #
305 305 # Wrapper for Project#project_tree
306 306 def project_tree(projects, &block)
307 307 Project.project_tree(projects, &block)
308 308 end
309 309
310 310 def principals_check_box_tags(name, principals)
311 311 s = ''
312 312 principals.sort.each do |principal|
313 313 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
314 314 end
315 315 s.html_safe
316 316 end
317 317
318 318 # Returns a string for users/groups option tags
319 319 def principals_options_for_select(collection, selected=nil)
320 320 s = ''
321 321 if collection.include?(User.current)
322 322 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
323 323 end
324 324 groups = ''
325 325 collection.sort.each do |element|
326 326 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
327 327 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
328 328 end
329 329 unless groups.empty?
330 330 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
331 331 end
332 332 s.html_safe
333 333 end
334 334
335 335 # Options for the new membership projects combo-box
336 336 def options_for_membership_project_select(principal, projects)
337 337 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
338 338 options << project_tree_options_for_select(projects) do |p|
339 339 {:disabled => principal.projects.include?(p)}
340 340 end
341 341 options
342 342 end
343 343
344 344 # Truncates and returns the string as a single line
345 345 def truncate_single_line(string, *args)
346 346 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
347 347 end
348 348
349 349 # Truncates at line break after 250 characters or options[:length]
350 350 def truncate_lines(string, options={})
351 351 length = options[:length] || 250
352 352 if string.to_s =~ /\A(.{#{length}}.*?)$/m
353 353 "#{$1}..."
354 354 else
355 355 string
356 356 end
357 357 end
358 358
359 359 def anchor(text)
360 360 text.to_s.gsub(' ', '_')
361 361 end
362 362
363 363 def html_hours(text)
364 364 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
365 365 end
366 366
367 367 def authoring(created, author, options={})
368 368 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
369 369 end
370 370
371 371 def time_tag(time)
372 372 text = distance_of_time_in_words(Time.now, time)
373 373 if @project
374 374 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
375 375 else
376 376 content_tag('acronym', text, :title => format_time(time))
377 377 end
378 378 end
379 379
380 380 def syntax_highlight_lines(name, content)
381 381 lines = []
382 382 syntax_highlight(name, content).each_line { |line| lines << line }
383 383 lines
384 384 end
385 385
386 386 def syntax_highlight(name, content)
387 387 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
388 388 end
389 389
390 390 def to_path_param(path)
391 391 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
392 392 str.blank? ? nil : str
393 393 end
394 394
395 395 def reorder_links(name, url, method = :post)
396 396 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
397 397 url.merge({"#{name}[move_to]" => 'highest'}),
398 398 :method => method, :title => l(:label_sort_highest)) +
399 399 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
400 400 url.merge({"#{name}[move_to]" => 'higher'}),
401 401 :method => method, :title => l(:label_sort_higher)) +
402 402 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
403 403 url.merge({"#{name}[move_to]" => 'lower'}),
404 404 :method => method, :title => l(:label_sort_lower)) +
405 405 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
406 406 url.merge({"#{name}[move_to]" => 'lowest'}),
407 407 :method => method, :title => l(:label_sort_lowest))
408 408 end
409 409
410 410 def breadcrumb(*args)
411 411 elements = args.flatten
412 412 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
413 413 end
414 414
415 415 def other_formats_links(&block)
416 416 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
417 417 yield Redmine::Views::OtherFormatsBuilder.new(self)
418 418 concat('</p>'.html_safe)
419 419 end
420 420
421 421 def page_header_title
422 422 if @project.nil? || @project.new_record?
423 423 h(Setting.app_title)
424 424 else
425 425 b = []
426 426 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
427 427 if ancestors.any?
428 428 root = ancestors.shift
429 429 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
430 430 if ancestors.size > 2
431 431 b << "\xe2\x80\xa6"
432 432 ancestors = ancestors[-2, 2]
433 433 end
434 434 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
435 435 end
436 436 b << h(@project)
437 437 b.join(" \xc2\xbb ").html_safe
438 438 end
439 439 end
440 440
441 441 def html_title(*args)
442 442 if args.empty?
443 443 title = @html_title || []
444 444 title << @project.name if @project
445 445 title << Setting.app_title unless Setting.app_title == title.last
446 446 title.select {|t| !t.blank? }.join(' - ')
447 447 else
448 448 @html_title ||= []
449 449 @html_title += args
450 450 end
451 451 end
452 452
453 453 # Returns the theme, controller name, and action as css classes for the
454 454 # HTML body.
455 455 def body_css_classes
456 456 css = []
457 457 if theme = Redmine::Themes.theme(Setting.ui_theme)
458 458 css << 'theme-' + theme.name
459 459 end
460 460
461 461 css << 'controller-' + controller_name
462 462 css << 'action-' + action_name
463 463 css.join(' ')
464 464 end
465 465
466 466 def accesskey(s)
467 467 Redmine::AccessKeys.key_for s
468 468 end
469 469
470 470 # Formats text according to system settings.
471 471 # 2 ways to call this method:
472 472 # * with a String: textilizable(text, options)
473 473 # * with an object and one of its attribute: textilizable(issue, :description, options)
474 474 def textilizable(*args)
475 475 options = args.last.is_a?(Hash) ? args.pop : {}
476 476 case args.size
477 477 when 1
478 478 obj = options[:object]
479 479 text = args.shift
480 480 when 2
481 481 obj = args.shift
482 482 attr = args.shift
483 483 text = obj.send(attr).to_s
484 484 else
485 485 raise ArgumentError, 'invalid arguments to textilizable'
486 486 end
487 487 return '' if text.blank?
488 488 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
489 489 only_path = options.delete(:only_path) == false ? false : true
490 490
491 491 text = text.dup
492 492 macros = catch_macros(text)
493 493 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
494 494
495 495 @parsed_headings = []
496 496 @heading_anchors = {}
497 497 @current_section = 0 if options[:edit_section_links]
498 498
499 499 parse_sections(text, project, obj, attr, only_path, options)
500 500 text = parse_non_pre_blocks(text, obj, macros) do |text|
501 501 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
502 502 send method_name, text, project, obj, attr, only_path, options
503 503 end
504 504 end
505 505 parse_headings(text, project, obj, attr, only_path, options)
506 506
507 507 if @parsed_headings.any?
508 508 replace_toc(text, @parsed_headings)
509 509 end
510 510
511 511 text.html_safe
512 512 end
513 513
514 514 def parse_non_pre_blocks(text, obj, macros)
515 515 s = StringScanner.new(text)
516 516 tags = []
517 517 parsed = ''
518 518 while !s.eos?
519 519 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
520 520 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
521 521 if tags.empty?
522 522 yield text
523 523 inject_macros(text, obj, macros) if macros.any?
524 524 else
525 525 inject_macros(text, obj, macros, false) if macros.any?
526 526 end
527 527 parsed << text
528 528 if tag
529 529 if closing
530 530 if tags.last == tag.downcase
531 531 tags.pop
532 532 end
533 533 else
534 534 tags << tag.downcase
535 535 end
536 536 parsed << full_tag
537 537 end
538 538 end
539 539 # Close any non closing tags
540 540 while tag = tags.pop
541 541 parsed << "</#{tag}>"
542 542 end
543 543 parsed
544 544 end
545 545
546 546 def parse_inline_attachments(text, project, obj, attr, only_path, options)
547 547 # when using an image link, try to use an attachment, if possible
548 548 attachments = options[:attachments] || []
549 549 attachments += obj.attachments if obj.respond_to?(:attachments)
550 550 if attachments.present?
551 551 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
552 552 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
553 553 # search for the picture in attachments
554 554 if found = Attachment.latest_attach(attachments, filename)
555 555 image_url = url_for :only_path => only_path, :controller => 'attachments',
556 556 :action => 'download', :id => found
557 557 desc = found.description.to_s.gsub('"', '')
558 558 if !desc.blank? && alttext.blank?
559 559 alt = " title=\"#{desc}\" alt=\"#{desc}\""
560 560 end
561 561 "src=\"#{image_url}\"#{alt}"
562 562 else
563 563 m
564 564 end
565 565 end
566 566 end
567 567 end
568 568
569 569 # Wiki links
570 570 #
571 571 # Examples:
572 572 # [[mypage]]
573 573 # [[mypage|mytext]]
574 574 # wiki links can refer other project wikis, using project name or identifier:
575 575 # [[project:]] -> wiki starting page
576 576 # [[project:|mytext]]
577 577 # [[project:mypage]]
578 578 # [[project:mypage|mytext]]
579 579 def parse_wiki_links(text, project, obj, attr, only_path, options)
580 580 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
581 581 link_project = project
582 582 esc, all, page, title = $1, $2, $3, $5
583 583 if esc.nil?
584 584 if page =~ /^([^\:]+)\:(.*)$/
585 585 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
586 586 page = $2
587 587 title ||= $1 if page.blank?
588 588 end
589 589
590 590 if link_project && link_project.wiki
591 591 # extract anchor
592 592 anchor = nil
593 593 if page =~ /^(.+?)\#(.+)$/
594 594 page, anchor = $1, $2
595 595 end
596 596 anchor = sanitize_anchor_name(anchor) if anchor.present?
597 597 # check if page exists
598 598 wiki_page = link_project.wiki.find_page(page)
599 599 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
600 600 "##{anchor}"
601 601 else
602 602 case options[:wiki_links]
603 603 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
604 604 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
605 605 else
606 606 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
607 607 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
608 608 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
609 609 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
610 610 end
611 611 end
612 612 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
613 613 else
614 614 # project or wiki doesn't exist
615 615 all
616 616 end
617 617 else
618 618 all
619 619 end
620 620 end
621 621 end
622 622
623 623 # Redmine links
624 624 #
625 625 # Examples:
626 626 # Issues:
627 627 # #52 -> Link to issue #52
628 628 # Changesets:
629 629 # r52 -> Link to revision 52
630 630 # commit:a85130f -> Link to scmid starting with a85130f
631 631 # Documents:
632 632 # document#17 -> Link to document with id 17
633 633 # document:Greetings -> Link to the document with title "Greetings"
634 634 # document:"Some document" -> Link to the document with title "Some document"
635 635 # Versions:
636 636 # version#3 -> Link to version with id 3
637 637 # version:1.0.0 -> Link to version named "1.0.0"
638 638 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
639 639 # Attachments:
640 640 # attachment:file.zip -> Link to the attachment of the current object named file.zip
641 641 # Source files:
642 642 # source:some/file -> Link to the file located at /some/file in the project's repository
643 643 # source:some/file@52 -> Link to the file's revision 52
644 644 # source:some/file#L120 -> Link to line 120 of the file
645 645 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
646 646 # export:some/file -> Force the download of the file
647 647 # Forum messages:
648 648 # message#1218 -> Link to message with id 1218
649 649 #
650 650 # Links can refer other objects from other projects, using project identifier:
651 651 # identifier:r52
652 652 # identifier:document:"Some document"
653 653 # identifier:version:1.0.0
654 654 # identifier:source:some/file
655 655 def parse_redmine_links(text, project, obj, attr, only_path, options)
656 656 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
657 657 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
658 658 link = nil
659 659 if project_identifier
660 660 project = Project.visible.find_by_identifier(project_identifier)
661 661 end
662 662 if esc.nil?
663 663 if prefix.nil? && sep == 'r'
664 664 if project
665 665 repository = nil
666 666 if repo_identifier
667 667 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
668 668 else
669 669 repository = project.repository
670 670 end
671 671 # project.changesets.visible raises an SQL error because of a double join on repositories
672 672 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
673 673 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
674 674 :class => 'changeset',
675 675 :title => truncate_single_line(changeset.comments, :length => 100))
676 676 end
677 677 end
678 678 elsif sep == '#'
679 679 oid = identifier.to_i
680 680 case prefix
681 681 when nil
682 682 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
683 683 anchor = comment_id ? "note-#{comment_id}" : nil
684 684 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
685 685 :class => issue.css_classes,
686 686 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
687 687 end
688 688 when 'document'
689 689 if document = Document.visible.find_by_id(oid)
690 690 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
691 691 :class => 'document'
692 692 end
693 693 when 'version'
694 694 if version = Version.visible.find_by_id(oid)
695 695 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
696 696 :class => 'version'
697 697 end
698 698 when 'message'
699 699 if message = Message.visible.find_by_id(oid, :include => :parent)
700 700 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
701 701 end
702 702 when 'forum'
703 703 if board = Board.visible.find_by_id(oid)
704 704 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
705 705 :class => 'board'
706 706 end
707 707 when 'news'
708 708 if news = News.visible.find_by_id(oid)
709 709 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
710 710 :class => 'news'
711 711 end
712 712 when 'project'
713 713 if p = Project.visible.find_by_id(oid)
714 714 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
715 715 end
716 716 end
717 717 elsif sep == ':'
718 718 # removes the double quotes if any
719 719 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
720 720 case prefix
721 721 when 'document'
722 722 if project && document = project.documents.visible.find_by_title(name)
723 723 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
724 724 :class => 'document'
725 725 end
726 726 when 'version'
727 727 if project && version = project.versions.visible.find_by_name(name)
728 728 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
729 729 :class => 'version'
730 730 end
731 731 when 'forum'
732 732 if project && board = project.boards.visible.find_by_name(name)
733 733 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
734 734 :class => 'board'
735 735 end
736 736 when 'news'
737 737 if project && news = project.news.visible.find_by_title(name)
738 738 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
739 739 :class => 'news'
740 740 end
741 741 when 'commit', 'source', 'export'
742 742 if project
743 743 repository = nil
744 744 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
745 745 repo_prefix, repo_identifier, name = $1, $2, $3
746 746 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
747 747 else
748 748 repository = project.repository
749 749 end
750 750 if prefix == 'commit'
751 751 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
752 752 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},
753 753 :class => 'changeset',
754 754 :title => truncate_single_line(h(changeset.comments), :length => 100)
755 755 end
756 756 else
757 757 if repository && User.current.allowed_to?(:browse_repository, project)
758 758 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
759 759 path, rev, anchor = $1, $3, $5
760 760 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
761 761 :path => to_path_param(path),
762 762 :rev => rev,
763 763 :anchor => anchor},
764 764 :class => (prefix == 'export' ? 'source download' : 'source')
765 765 end
766 766 end
767 767 repo_prefix = nil
768 768 end
769 769 when 'attachment'
770 770 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
771 771 if attachments && attachment = attachments.detect {|a| a.filename == name }
772 772 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
773 773 :class => 'attachment'
774 774 end
775 775 when 'project'
776 776 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
777 777 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
778 778 end
779 779 end
780 780 end
781 781 end
782 782 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
783 783 end
784 784 end
785 785
786 786 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
787 787
788 788 def parse_sections(text, project, obj, attr, only_path, options)
789 789 return unless options[:edit_section_links]
790 790 text.gsub!(HEADING_RE) do
791 791 heading = $1
792 792 @current_section += 1
793 793 if @current_section > 1
794 794 content_tag('div',
795 795 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
796 796 :class => 'contextual',
797 797 :title => l(:button_edit_section)) + heading.html_safe
798 798 else
799 799 heading
800 800 end
801 801 end
802 802 end
803 803
804 804 # Headings and TOC
805 805 # Adds ids and links to headings unless options[:headings] is set to false
806 806 def parse_headings(text, project, obj, attr, only_path, options)
807 807 return if options[:headings] == false
808 808
809 809 text.gsub!(HEADING_RE) do
810 810 level, attrs, content = $2.to_i, $3, $4
811 811 item = strip_tags(content).strip
812 812 anchor = sanitize_anchor_name(item)
813 813 # used for single-file wiki export
814 814 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
815 815 @heading_anchors[anchor] ||= 0
816 816 idx = (@heading_anchors[anchor] += 1)
817 817 if idx > 1
818 818 anchor = "#{anchor}-#{idx}"
819 819 end
820 820 @parsed_headings << [level, anchor, item]
821 821 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
822 822 end
823 823 end
824 824
825 825 MACROS_RE = /(
826 826 (!)? # escaping
827 827 (
828 828 \{\{ # opening tag
829 829 ([\w]+) # macro name
830 830 (\(([^\n\r]*?)\))? # optional arguments
831 831 ([\n\r].*?[\n\r])? # optional block of text
832 832 \}\} # closing tag
833 833 )
834 834 )/mx unless const_defined?(:MACROS_RE)
835 835
836 836 MACRO_SUB_RE = /(
837 837 \{\{
838 838 macro\((\d+)\)
839 839 \}\}
840 840 )/x unless const_defined?(:MACRO_SUB_RE)
841 841
842 842 # Extracts macros from text
843 843 def catch_macros(text)
844 844 macros = {}
845 845 text.gsub!(MACROS_RE) do
846 846 all, macro = $1, $4.downcase
847 847 if macro_exists?(macro) || all =~ MACRO_SUB_RE
848 848 index = macros.size
849 849 macros[index] = all
850 850 "{{macro(#{index})}}"
851 851 else
852 852 all
853 853 end
854 854 end
855 855 macros
856 856 end
857 857
858 858 # Executes and replaces macros in text
859 859 def inject_macros(text, obj, macros, execute=true)
860 860 text.gsub!(MACRO_SUB_RE) do
861 861 all, index = $1, $2.to_i
862 862 orig = macros.delete(index)
863 863 if execute && orig && orig =~ MACROS_RE
864 864 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
865 865 if esc.nil?
866 866 h(exec_macro(macro, obj, args, block) || all)
867 867 else
868 868 h(all)
869 869 end
870 870 elsif orig
871 871 h(orig)
872 872 else
873 873 h(all)
874 874 end
875 875 end
876 876 end
877 877
878 878 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
879 879
880 880 # Renders the TOC with given headings
881 881 def replace_toc(text, headings)
882 882 text.gsub!(TOC_RE) do
883 883 # Keep only the 4 first levels
884 884 headings = headings.select{|level, anchor, item| level <= 4}
885 885 if headings.empty?
886 886 ''
887 887 else
888 888 div_class = 'toc'
889 889 div_class << ' right' if $1 == '>'
890 890 div_class << ' left' if $1 == '<'
891 891 out = "<ul class=\"#{div_class}\"><li>"
892 892 root = headings.map(&:first).min
893 893 current = root
894 894 started = false
895 895 headings.each do |level, anchor, item|
896 896 if level > current
897 897 out << '<ul><li>' * (level - current)
898 898 elsif level < current
899 899 out << "</li></ul>\n" * (current - level) + "</li><li>"
900 900 elsif started
901 901 out << '</li><li>'
902 902 end
903 903 out << "<a href=\"##{anchor}\">#{item}</a>"
904 904 current = level
905 905 started = true
906 906 end
907 907 out << '</li></ul>' * (current - root)
908 908 out << '</li></ul>'
909 909 end
910 910 end
911 911 end
912 912
913 913 # Same as Rails' simple_format helper without using paragraphs
914 914 def simple_format_without_paragraph(text)
915 915 text.to_s.
916 916 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
917 917 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
918 918 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
919 919 html_safe
920 920 end
921 921
922 922 def lang_options_for_select(blank=true)
923 923 (blank ? [["(auto)", ""]] : []) + languages_options
924 924 end
925 925
926 926 def label_tag_for(name, option_tags = nil, options = {})
927 927 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
928 928 content_tag("label", label_text)
929 929 end
930 930
931 931 def labelled_form_for(*args, &proc)
932 932 args << {} unless args.last.is_a?(Hash)
933 933 options = args.last
934 934 if args.first.is_a?(Symbol)
935 935 options.merge!(:as => args.shift)
936 936 end
937 937 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
938 938 form_for(*args, &proc)
939 939 end
940 940
941 941 def labelled_fields_for(*args, &proc)
942 942 args << {} unless args.last.is_a?(Hash)
943 943 options = args.last
944 944 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
945 945 fields_for(*args, &proc)
946 946 end
947 947
948 948 def labelled_remote_form_for(*args, &proc)
949 949 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
950 950 args << {} unless args.last.is_a?(Hash)
951 951 options = args.last
952 952 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
953 953 form_for(*args, &proc)
954 954 end
955 955
956 956 def error_messages_for(*objects)
957 957 html = ""
958 958 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
959 959 errors = objects.map {|o| o.errors.full_messages}.flatten
960 960 if errors.any?
961 961 html << "<div id='errorExplanation'><ul>\n"
962 962 errors.each do |error|
963 963 html << "<li>#{h error}</li>\n"
964 964 end
965 965 html << "</ul></div>\n"
966 966 end
967 967 html.html_safe
968 968 end
969 969
970 970 def delete_link(url, options={})
971 971 options = {
972 972 :method => :delete,
973 973 :data => {:confirm => l(:text_are_you_sure)},
974 974 :class => 'icon icon-del'
975 975 }.merge(options)
976 976
977 977 link_to l(:button_delete), url, options
978 978 end
979 979
980 980 def preview_link(url, form, target='preview', options={})
981 981 content_tag 'a', l(:label_preview), {
982 982 :href => "#",
983 983 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
984 984 :accesskey => accesskey(:preview)
985 985 }.merge(options)
986 986 end
987 987
988 988 def link_to_function(name, function, html_options={})
989 989 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
990 990 end
991 991
992 992 # Helper to render JSON in views
993 993 def raw_json(arg)
994 994 arg.to_json.to_s.gsub('/', '\/').html_safe
995 995 end
996 996
997 997 def back_url
998 998 url = params[:back_url]
999 999 if url.nil? && referer = request.env['HTTP_REFERER']
1000 1000 url = CGI.unescape(referer.to_s)
1001 1001 end
1002 1002 url
1003 1003 end
1004 1004
1005 1005 def back_url_hidden_field_tag
1006 1006 url = back_url
1007 1007 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1008 1008 end
1009 1009
1010 1010 def check_all_links(form_name)
1011 1011 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1012 1012 " | ".html_safe +
1013 1013 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1014 1014 end
1015 1015
1016 1016 def progress_bar(pcts, options={})
1017 1017 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1018 1018 pcts = pcts.collect(&:round)
1019 1019 pcts[1] = pcts[1] - pcts[0]
1020 1020 pcts << (100 - pcts[1] - pcts[0])
1021 1021 width = options[:width] || '100px;'
1022 1022 legend = options[:legend] || ''
1023 1023 content_tag('table',
1024 1024 content_tag('tr',
1025 1025 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1026 1026 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1027 1027 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1028 1028 ), :class => 'progress', :style => "width: #{width};").html_safe +
1029 1029 content_tag('p', legend, :class => 'percent').html_safe
1030 1030 end
1031 1031
1032 1032 def checked_image(checked=true)
1033 1033 if checked
1034 1034 image_tag 'toggle_check.png'
1035 1035 end
1036 1036 end
1037 1037
1038 1038 def context_menu(url)
1039 1039 unless @context_menu_included
1040 1040 content_for :header_tags do
1041 1041 javascript_include_tag('context_menu') +
1042 1042 stylesheet_link_tag('context_menu')
1043 1043 end
1044 1044 if l(:direction) == 'rtl'
1045 1045 content_for :header_tags do
1046 1046 stylesheet_link_tag('context_menu_rtl')
1047 1047 end
1048 1048 end
1049 1049 @context_menu_included = true
1050 1050 end
1051 1051 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1052 1052 end
1053 1053
1054 1054 def calendar_for(field_id)
1055 1055 include_calendar_headers_tags
1056 1056 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1057 1057 end
1058 1058
1059 1059 def include_calendar_headers_tags
1060 1060 unless @calendar_headers_tags_included
1061 1061 @calendar_headers_tags_included = true
1062 1062 content_for :header_tags do
1063 1063 start_of_week = Setting.start_of_week
1064 1064 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1065 1065 # Redmine uses 1..7 (monday..sunday) in settings and locales
1066 1066 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1067 1067 start_of_week = start_of_week.to_i % 7
1068 1068
1069 1069 tags = javascript_tag(
1070 1070 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1071 1071 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1072 1072 path_to_image('/images/calendar.png') +
1073 1073 "', showButtonPanel: true};")
1074 1074 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1075 1075 unless jquery_locale == 'en'
1076 1076 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1077 1077 end
1078 1078 tags
1079 1079 end
1080 1080 end
1081 1081 end
1082 1082
1083 1083 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1084 1084 # Examples:
1085 1085 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1086 1086 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1087 1087 #
1088 1088 def stylesheet_link_tag(*sources)
1089 1089 options = sources.last.is_a?(Hash) ? sources.pop : {}
1090 1090 plugin = options.delete(:plugin)
1091 1091 sources = sources.map do |source|
1092 1092 if plugin
1093 1093 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1094 1094 elsif current_theme && current_theme.stylesheets.include?(source)
1095 1095 current_theme.stylesheet_path(source)
1096 1096 else
1097 1097 source
1098 1098 end
1099 1099 end
1100 1100 super sources, options
1101 1101 end
1102 1102
1103 1103 # Overrides Rails' image_tag with themes and plugins support.
1104 1104 # Examples:
1105 1105 # image_tag('image.png') # => picks image.png from the current theme or defaults
1106 1106 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1107 1107 #
1108 1108 def image_tag(source, options={})
1109 1109 if plugin = options.delete(:plugin)
1110 1110 source = "/plugin_assets/#{plugin}/images/#{source}"
1111 1111 elsif current_theme && current_theme.images.include?(source)
1112 1112 source = current_theme.image_path(source)
1113 1113 end
1114 1114 super source, options
1115 1115 end
1116 1116
1117 1117 # Overrides Rails' javascript_include_tag with plugins support
1118 1118 # Examples:
1119 1119 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1120 1120 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1121 1121 #
1122 1122 def javascript_include_tag(*sources)
1123 1123 options = sources.last.is_a?(Hash) ? sources.pop : {}
1124 1124 if plugin = options.delete(:plugin)
1125 1125 sources = sources.map do |source|
1126 1126 if plugin
1127 1127 "/plugin_assets/#{plugin}/javascripts/#{source}"
1128 1128 else
1129 1129 source
1130 1130 end
1131 1131 end
1132 1132 end
1133 1133 super sources, options
1134 1134 end
1135 1135
1136 1136 def content_for(name, content = nil, &block)
1137 1137 @has_content ||= {}
1138 1138 @has_content[name] = true
1139 1139 super(name, content, &block)
1140 1140 end
1141 1141
1142 1142 def has_content?(name)
1143 1143 (@has_content && @has_content[name]) || false
1144 1144 end
1145 1145
1146 1146 def sidebar_content?
1147 1147 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1148 1148 end
1149 1149
1150 1150 def view_layouts_base_sidebar_hook_response
1151 1151 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1152 1152 end
1153 1153
1154 1154 def email_delivery_enabled?
1155 1155 !!ActionMailer::Base.perform_deliveries
1156 1156 end
1157 1157
1158 1158 # Returns the avatar image tag for the given +user+ if avatars are enabled
1159 1159 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1160 1160 def avatar(user, options = { })
1161 1161 if Setting.gravatar_enabled?
1162 1162 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1163 1163 email = nil
1164 1164 if user.respond_to?(:mail)
1165 1165 email = user.mail
1166 1166 elsif user.to_s =~ %r{<(.+?)>}
1167 1167 email = $1
1168 1168 end
1169 1169 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1170 1170 else
1171 1171 ''
1172 1172 end
1173 1173 end
1174 1174
1175 1175 def sanitize_anchor_name(anchor)
1176 1176 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1177 1177 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1178 1178 else
1179 1179 # TODO: remove when ruby1.8 is no longer supported
1180 1180 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1181 1181 end
1182 1182 end
1183 1183
1184 1184 # Returns the javascript tags that are included in the html layout head
1185 1185 def javascript_heads
1186 1186 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1187 1187 unless User.current.pref.warn_on_leaving_unsaved == '0'
1188 1188 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1189 1189 end
1190 1190 tags
1191 1191 end
1192 1192
1193 1193 def favicon
1194 1194 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1195 1195 end
1196 1196
1197 1197 def robot_exclusion_tag
1198 1198 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1199 1199 end
1200 1200
1201 1201 # Returns true if arg is expected in the API response
1202 1202 def include_in_api_response?(arg)
1203 1203 unless @included_in_api_response
1204 1204 param = params[:include]
1205 1205 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1206 1206 @included_in_api_response.collect!(&:strip)
1207 1207 end
1208 1208 @included_in_api_response.include?(arg.to_s)
1209 1209 end
1210 1210
1211 1211 # Returns options or nil if nometa param or X-Redmine-Nometa header
1212 1212 # was set in the request
1213 1213 def api_meta(options)
1214 1214 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1215 1215 # compatibility mode for activeresource clients that raise
1216 1216 # an error when unserializing an array with attributes
1217 1217 nil
1218 1218 else
1219 1219 options
1220 1220 end
1221 1221 end
1222 1222
1223 1223 private
1224 1224
1225 1225 def wiki_helper
1226 1226 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1227 1227 extend helper
1228 1228 return self
1229 1229 end
1230 1230
1231 1231 def link_to_content_update(text, url_params = {}, html_options = {})
1232 1232 link_to(text, url_params, html_options)
1233 1233 end
1234 1234 end
@@ -1,47 +1,47
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module AttachmentsHelper
21 21 # Displays view/delete links to the attachments of the given object
22 22 # Options:
23 23 # :author -- author names are not displayed if set to false
24 24 # :thumbails -- display thumbnails if enabled in settings
25 25 def link_to_attachments(container, options = {})
26 26 options.assert_valid_keys(:author, :thumbnails)
27 27
28 28 if container.attachments.any?
29 29 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
30 30 render :partial => 'attachments/links',
31 31 :locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
32 32 end
33 33 end
34 34
35 35 def render_api_attachment(attachment, api)
36 36 api.attachment do
37 37 api.id attachment.id
38 38 api.filename attachment.filename
39 39 api.filesize attachment.filesize
40 40 api.content_type attachment.content_type
41 41 api.description attachment.description
42 42 api.content_url url_for(:controller => 'attachments', :action => 'download', :id => attachment, :filename => attachment.filename, :only_path => false)
43 43 api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
44 44 api.created_on attachment.created_on
45 45 end
46 46 end
47 47 end
@@ -1,24 +1,24
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module CalendarsHelper
21 21 def link_to_previous_month(year, month, options={})
22 22 target_year, target_month = if month == 1
23 23 [year - 1, 12]
24 24 else
25 25 [year, month - 1]
26 26 end
27 27
28 28 name = if target_month == 12
29 29 "#{month_name(target_month)} #{target_year}"
30 30 else
31 31 "#{month_name(target_month)}"
32 32 end
33 33
34 34 # \xc2\xab(utf-8) = &#171;
35 35 link_to_month(("\xc2\xab " + name), target_year, target_month, options)
36 36 end
37 37
38 38 def link_to_next_month(year, month, options={})
39 39 target_year, target_month = if month == 12
40 40 [year + 1, 1]
41 41 else
42 42 [year, month + 1]
43 43 end
44 44
45 45 name = if target_month == 1
46 46 "#{month_name(target_month)} #{target_year}"
47 47 else
48 48 "#{month_name(target_month)}"
49 49 end
50 50
51 51 # \xc2\xbb(utf-8) = &#187;
52 52 link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
53 53 end
54 54
55 55 def link_to_month(link_name, year, month, options={})
56 56 link_to_content_update(h(link_name), params.merge(:year => year, :month => month))
57 57 end
58 58 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ContextMenusHelper
21 21 def context_menu_link(name, url, options={})
22 22 options[:class] ||= ''
23 23 if options.delete(:selected)
24 24 options[:class] << ' icon-checked disabled'
25 25 options[:disabled] = true
26 26 end
27 27 if options.delete(:disabled)
28 28 options.delete(:method)
29 29 options.delete(:data)
30 30 options[:onclick] = 'return false;'
31 31 options[:class] << ' disabled'
32 32 url = '#'
33 33 end
34 34 link_to h(name), url, options
35 35 end
36 36
37 37 def bulk_update_custom_field_context_menu_link(field, text, value)
38 38 context_menu_link h(text),
39 39 bulk_update_issues_path(:ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back),
40 40 :method => :post,
41 41 :selected => (@issue && @issue.custom_field_value(field) == value)
42 42 end
43 43 end
@@ -1,149 +1,149
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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 def custom_fields_tabs
23 23 CustomField::CUSTOM_FIELDS_TABS
24 24 end
25 25
26 26 # Return custom field html tag corresponding to its format
27 27 def custom_field_tag(name, custom_value)
28 28 custom_field = custom_value.custom_field
29 29 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
30 30 field_name << "[]" if custom_field.multiple?
31 31 field_id = "#{name}_custom_field_values_#{custom_field.id}"
32 32
33 33 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
34 34
35 35 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
36 36 case field_format.try(:edit_as)
37 37 when "date"
38 38 text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
39 39 calendar_for(field_id)
40 40 when "text"
41 41 text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
42 42 when "bool"
43 43 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
44 44 when "list"
45 45 blank_option = ''.html_safe
46 46 unless custom_field.multiple?
47 47 if custom_field.is_required?
48 48 unless custom_field.default_value.present?
49 49 blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
50 50 end
51 51 else
52 52 blank_option = content_tag('option')
53 53 end
54 54 end
55 55 s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
56 56 tag_options.merge(:multiple => custom_field.multiple?))
57 57 if custom_field.multiple?
58 58 s << hidden_field_tag(field_name, '')
59 59 end
60 60 s
61 61 else
62 62 text_field_tag(field_name, custom_value.value, tag_options)
63 63 end
64 64 end
65 65
66 66 # Return custom field label tag
67 67 def custom_field_label_tag(name, custom_value, options={})
68 68 required = options[:required] || custom_value.custom_field.is_required?
69 69
70 70 content_tag "label", h(custom_value.custom_field.name) +
71 71 (required ? " <span class=\"required\">*</span>".html_safe : ""),
72 72 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
73 73 end
74 74
75 75 # Return custom field tag with its label tag
76 76 def custom_field_tag_with_label(name, custom_value, options={})
77 77 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
78 78 end
79 79
80 80 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
81 81 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
82 82 field_name << "[]" if custom_field.multiple?
83 83 field_id = "#{name}_custom_field_values_#{custom_field.id}"
84 84
85 85 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
86 86
87 87 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
88 88 case field_format.try(:edit_as)
89 89 when "date"
90 90 text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
91 91 calendar_for(field_id)
92 92 when "text"
93 93 text_area_tag(field_name, '', tag_options.merge(:rows => 3))
94 94 when "bool"
95 95 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
96 96 [l(:general_text_yes), '1'],
97 97 [l(:general_text_no), '0']]), tag_options)
98 98 when "list"
99 99 options = []
100 100 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
101 101 options << [l(:label_none), '__none__'] unless custom_field.is_required?
102 102 options += custom_field.possible_values_options(projects)
103 103 select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
104 104 else
105 105 text_field_tag(field_name, '', tag_options)
106 106 end
107 107 end
108 108
109 109 # Return a string used to display a custom value
110 110 def show_value(custom_value)
111 111 return "" unless custom_value
112 112 format_value(custom_value.value, custom_value.custom_field.field_format)
113 113 end
114 114
115 115 # Return a string used to display a custom value
116 116 def format_value(value, field_format)
117 117 if value.is_a?(Array)
118 118 value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
119 119 else
120 120 Redmine::CustomFieldFormat.format_value(value, field_format)
121 121 end
122 122 end
123 123
124 124 # Return an array of custom field formats which can be used in select_tag
125 125 def custom_field_formats_for_select(custom_field)
126 126 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
127 127 end
128 128
129 129 # Renders the custom_values in api views
130 130 def render_api_custom_values(custom_values, api)
131 131 api.array :custom_fields do
132 132 custom_values.each do |custom_value|
133 133 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
134 134 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
135 135 api.custom_field attrs do
136 136 if custom_value.value.is_a?(Array)
137 137 api.array :value do
138 138 custom_value.value.each do |value|
139 139 api.value value unless value.blank?
140 140 end
141 141 end
142 142 else
143 143 api.value custom_value.value
144 144 end
145 145 end
146 146 end
147 147 end unless custom_values.empty?
148 148 end
149 149 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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,27 +1,27
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module GroupsHelper
21 21 def group_settings_tabs
22 22 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
23 23 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
24 24 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
25 25 ]
26 26 end
27 27 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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,413 +1,413
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssuesHelper
21 21 include ApplicationHelper
22 22
23 23 def issue_list(issues, &block)
24 24 ancestors = []
25 25 issues.each do |issue|
26 26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 27 ancestors.pop
28 28 end
29 29 yield issue, ancestors.size
30 30 ancestors << issue unless issue.leaf?
31 31 end
32 32 end
33 33
34 34 # Renders a HTML/CSS tooltip
35 35 #
36 36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 37 # that contains this method wrapped in a span with the class of "tip"
38 38 #
39 39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 41 # </div>
42 42 #
43 43 def render_issue_tooltip(issue)
44 44 @cached_label_status ||= l(:field_status)
45 45 @cached_label_start_date ||= l(:field_start_date)
46 46 @cached_label_due_date ||= l(:field_due_date)
47 47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 48 @cached_label_priority ||= l(:field_priority)
49 49 @cached_label_project ||= l(:field_project)
50 50
51 51 link_to_issue(issue) + "<br /><br />".html_safe +
52 52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 58 end
59 59
60 60 def issue_heading(issue)
61 61 h("#{issue.tracker} ##{issue.id}")
62 62 end
63 63
64 64 def render_issue_subject_with_tree(issue)
65 65 s = ''
66 66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 67 ancestors.each do |ancestor|
68 68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 69 end
70 70 s << '<div>'
71 71 subject = h(issue.subject)
72 72 if issue.is_private?
73 73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 74 end
75 75 s << content_tag('h3', subject)
76 76 s << '</div>' * (ancestors.size + 1)
77 77 s.html_safe
78 78 end
79 79
80 80 def render_descendants_tree(issue)
81 81 s = '<form><table class="list issues">'
82 82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 83 css = "issue issue-#{child.id} hascontextmenu"
84 84 css << " idnt idnt-#{level}" if level > 0
85 85 s << content_tag('tr',
86 86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 88 content_tag('td', h(child.status)) +
89 89 content_tag('td', link_to_user(child.assigned_to)) +
90 90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 91 :class => css)
92 92 end
93 93 s << '</table></form>'
94 94 s.html_safe
95 95 end
96 96
97 97 # Returns a link for adding a new subtask to the given issue
98 98 def link_to_new_subtask(issue)
99 99 attrs = {
100 100 :tracker_id => issue.tracker,
101 101 :parent_issue_id => issue
102 102 }
103 103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
104 104 end
105 105
106 106 class IssueFieldsRows
107 107 include ActionView::Helpers::TagHelper
108 108
109 109 def initialize
110 110 @left = []
111 111 @right = []
112 112 end
113 113
114 114 def left(*args)
115 115 args.any? ? @left << cells(*args) : @left
116 116 end
117 117
118 118 def right(*args)
119 119 args.any? ? @right << cells(*args) : @right
120 120 end
121 121
122 122 def size
123 123 @left.size > @right.size ? @left.size : @right.size
124 124 end
125 125
126 126 def to_html
127 127 html = ''.html_safe
128 128 blank = content_tag('th', '') + content_tag('td', '')
129 129 size.times do |i|
130 130 left = @left[i] || blank
131 131 right = @right[i] || blank
132 132 html << content_tag('tr', left + right)
133 133 end
134 134 html
135 135 end
136 136
137 137 def cells(label, text, options={})
138 138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
139 139 end
140 140 end
141 141
142 142 def issue_fields_rows
143 143 r = IssueFieldsRows.new
144 144 yield r
145 145 r.to_html
146 146 end
147 147
148 148 def render_custom_fields_rows(issue)
149 149 return if issue.custom_field_values.empty?
150 150 ordered_values = []
151 151 half = (issue.custom_field_values.size / 2.0).ceil
152 152 half.times do |i|
153 153 ordered_values << issue.custom_field_values[i]
154 154 ordered_values << issue.custom_field_values[i + half]
155 155 end
156 156 s = "<tr>\n"
157 157 n = 0
158 158 ordered_values.compact.each do |value|
159 159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
160 160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
161 161 n += 1
162 162 end
163 163 s << "</tr>\n"
164 164 s.html_safe
165 165 end
166 166
167 167 def issues_destroy_confirmation_message(issues)
168 168 issues = [issues] unless issues.is_a?(Array)
169 169 message = l(:text_issues_destroy_confirmation)
170 170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
171 171 if descendant_count > 0
172 172 issues.each do |issue|
173 173 next if issue.root?
174 174 issues.each do |other_issue|
175 175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
176 176 end
177 177 end
178 178 if descendant_count > 0
179 179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
180 180 end
181 181 end
182 182 message
183 183 end
184 184
185 185 def sidebar_queries
186 186 unless @sidebar_queries
187 187 @sidebar_queries = IssueQuery.visible.all(
188 188 :order => "#{Query.table_name}.name ASC",
189 189 # Project specific queries and global queries
190 190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
191 191 )
192 192 end
193 193 @sidebar_queries
194 194 end
195 195
196 196 def query_links(title, queries)
197 197 # links to #index on issues/show
198 198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
199 199
200 200 content_tag('h3', h(title)) +
201 201 queries.collect {|query|
202 202 css = 'query'
203 203 css << ' selected' if query == @query
204 204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
205 205 }.join('<br />').html_safe
206 206 end
207 207
208 208 def render_sidebar_queries
209 209 out = ''.html_safe
210 210 queries = sidebar_queries.select {|q| !q.is_public?}
211 211 out << query_links(l(:label_my_queries), queries) if queries.any?
212 212 queries = sidebar_queries.select {|q| q.is_public?}
213 213 out << query_links(l(:label_query_plural), queries) if queries.any?
214 214 out
215 215 end
216 216
217 217 # Returns the textual representation of a journal details
218 218 # as an array of strings
219 219 def details_to_strings(details, no_html=false, options={})
220 220 options[:only_path] = (options[:only_path] == false ? false : true)
221 221 strings = []
222 222 values_by_field = {}
223 223 details.each do |detail|
224 224 if detail.property == 'cf'
225 225 field_id = detail.prop_key
226 226 field = CustomField.find_by_id(field_id)
227 227 if field && field.multiple?
228 228 values_by_field[field_id] ||= {:added => [], :deleted => []}
229 229 if detail.old_value
230 230 values_by_field[field_id][:deleted] << detail.old_value
231 231 end
232 232 if detail.value
233 233 values_by_field[field_id][:added] << detail.value
234 234 end
235 235 next
236 236 end
237 237 end
238 238 strings << show_detail(detail, no_html, options)
239 239 end
240 240 values_by_field.each do |field_id, changes|
241 241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
242 242 if changes[:added].any?
243 243 detail.value = changes[:added]
244 244 strings << show_detail(detail, no_html, options)
245 245 elsif changes[:deleted].any?
246 246 detail.old_value = changes[:deleted]
247 247 strings << show_detail(detail, no_html, options)
248 248 end
249 249 end
250 250 strings
251 251 end
252 252
253 253 # Returns the textual representation of a single journal detail
254 254 def show_detail(detail, no_html=false, options={})
255 255 multiple = false
256 256 case detail.property
257 257 when 'attr'
258 258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
259 259 label = l(("field_" + field).to_sym)
260 260 case detail.prop_key
261 261 when 'due_date', 'start_date'
262 262 value = format_date(detail.value.to_date) if detail.value
263 263 old_value = format_date(detail.old_value.to_date) if detail.old_value
264 264
265 265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
266 266 'priority_id', 'category_id', 'fixed_version_id'
267 267 value = find_name_by_reflection(field, detail.value)
268 268 old_value = find_name_by_reflection(field, detail.old_value)
269 269
270 270 when 'estimated_hours'
271 271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
272 272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
273 273
274 274 when 'parent_id'
275 275 label = l(:field_parent_issue)
276 276 value = "##{detail.value}" unless detail.value.blank?
277 277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
278 278
279 279 when 'is_private'
280 280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
281 281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
282 282 end
283 283 when 'cf'
284 284 custom_field = CustomField.find_by_id(detail.prop_key)
285 285 if custom_field
286 286 multiple = custom_field.multiple?
287 287 label = custom_field.name
288 288 value = format_value(detail.value, custom_field.field_format) if detail.value
289 289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
290 290 end
291 291 when 'attachment'
292 292 label = l(:label_attachment)
293 293 end
294 294 call_hook(:helper_issues_show_detail_after_setting,
295 295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
296 296
297 297 label ||= detail.prop_key
298 298 value ||= detail.value
299 299 old_value ||= detail.old_value
300 300
301 301 unless no_html
302 302 label = content_tag('strong', label)
303 303 old_value = content_tag("i", h(old_value)) if detail.old_value
304 304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
305 305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
306 306 # Link to the attachment if it has not been removed
307 307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
308 308 if options[:only_path] != false && atta.is_text?
309 309 value += link_to(
310 310 image_tag('magnifier.png'),
311 311 :controller => 'attachments', :action => 'show',
312 312 :id => atta, :filename => atta.filename
313 313 )
314 314 end
315 315 else
316 316 value = content_tag("i", h(value)) if value
317 317 end
318 318 end
319 319
320 320 if detail.property == 'attr' && detail.prop_key == 'description'
321 321 s = l(:text_journal_changed_no_detail, :label => label)
322 322 unless no_html
323 323 diff_link = link_to 'diff',
324 324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
325 325 :detail_id => detail.id, :only_path => options[:only_path]},
326 326 :title => l(:label_view_diff)
327 327 s << " (#{ diff_link })"
328 328 end
329 329 s.html_safe
330 330 elsif detail.value.present?
331 331 case detail.property
332 332 when 'attr', 'cf'
333 333 if detail.old_value.present?
334 334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
335 335 elsif multiple
336 336 l(:text_journal_added, :label => label, :value => value).html_safe
337 337 else
338 338 l(:text_journal_set_to, :label => label, :value => value).html_safe
339 339 end
340 340 when 'attachment'
341 341 l(:text_journal_added, :label => label, :value => value).html_safe
342 342 end
343 343 else
344 344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
345 345 end
346 346 end
347 347
348 348 # Find the name of an associated record stored in the field attribute
349 349 def find_name_by_reflection(field, id)
350 350 unless id.present?
351 351 return nil
352 352 end
353 353 association = Issue.reflect_on_association(field.to_sym)
354 354 if association
355 355 record = association.class_name.constantize.find_by_id(id)
356 356 return record.name if record
357 357 end
358 358 end
359 359
360 360 # Renders issue children recursively
361 361 def render_api_issue_children(issue, api)
362 362 return if issue.leaf?
363 363 api.array :children do
364 364 issue.children.each do |child|
365 365 api.issue(:id => child.id) do
366 366 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
367 367 api.subject child.subject
368 368 render_api_issue_children(child, api)
369 369 end
370 370 end
371 371 end
372 372 end
373 373
374 374 def issues_to_csv(issues, project, query, options={})
375 375 decimal_separator = l(:general_csv_decimal_separator)
376 376 encoding = l(:general_csv_encoding)
377 377 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
378 378 if options[:description]
379 379 if description = query.available_columns.detect {|q| q.name == :description}
380 380 columns << description
381 381 end
382 382 end
383 383
384 384 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
385 385 # csv header fields
386 386 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
387 387
388 388 # csv lines
389 389 issues.each do |issue|
390 390 col_values = columns.collect do |column|
391 391 s = if column.is_a?(QueryCustomFieldColumn)
392 392 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
393 393 show_value(cv)
394 394 else
395 395 value = column.value(issue)
396 396 if value.is_a?(Date)
397 397 format_date(value)
398 398 elsif value.is_a?(Time)
399 399 format_time(value)
400 400 elsif value.is_a?(Float)
401 401 ("%.2f" % value).gsub('.', decimal_separator)
402 402 else
403 403 value
404 404 end
405 405 end
406 406 s.to_s
407 407 end
408 408 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
409 409 end
410 410 end
411 411 export
412 412 end
413 413 end
@@ -1,46 +1,46
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MessagesHelper
21 21 end
@@ -1,71 +1,71
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module MyHelper
21 21 def calendar_items(startdt, enddt)
22 22 Issue.visible.
23 23 where(:project_id => User.current.projects.map(&:id)).
24 24 where("(start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)", startdt, enddt, startdt, enddt).
25 25 includes(:project, :tracker, :priority, :assigned_to).
26 26 all
27 27 end
28 28
29 29 def documents_items
30 30 Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).all
31 31 end
32 32
33 33 def issuesassignedtome_items
34 34 Issue.visible.open.
35 35 where(:assigned_to_id => ([User.current.id] + User.current.group_ids)).
36 36 limit(10).
37 37 includes(:status, :project, :tracker, :priority).
38 38 order("#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC").
39 39 all
40 40 end
41 41
42 42 def issuesreportedbyme_items
43 43 Issue.visible.
44 44 where(:author_id => User.current.id).
45 45 limit(10).
46 46 includes(:status, :project, :tracker).
47 47 order("#{Issue.table_name}.updated_on DESC").
48 48 all
49 49 end
50 50
51 51 def issueswatched_items
52 52 Issue.visible.on_active_project.watched_by(User.current.id).recently_updated.limit(10).all
53 53 end
54 54
55 55 def news_items
56 56 News.visible.
57 57 where(:project_id => User.current.projects.map(&:id)).
58 58 limit(10).
59 59 includes(:project, :author).
60 60 order("#{News.table_name}.created_on DESC").
61 61 all
62 62 end
63 63
64 64 def timelog_items
65 65 TimeEntry.
66 66 where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, Date.today - 6, Date.today).
67 67 includes(:activity, :project, {:issue => [:tracker, :status]}).
68 68 order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC").
69 69 all
70 70 end
71 71 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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,83 +1,83
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module ProjectsHelper
21 21 def link_to_version(version, options = {})
22 22 return '' unless version && version.is_a?(Version)
23 23 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
24 24 end
25 25
26 26 def project_settings_tabs
27 27 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
28 28 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
29 29 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
30 30 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
31 31 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
32 32 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
33 33 {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
34 34 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
35 35 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
36 36 ]
37 37 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
38 38 end
39 39
40 40 def parent_project_select_tag(project)
41 41 selected = project.parent
42 42 # retrieve the requested parent project
43 43 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
44 44 if parent_id
45 45 selected = (parent_id.blank? ? nil : Project.find(parent_id))
46 46 end
47 47
48 48 options = ''
49 49 options << "<option value=''></option>" if project.allowed_parents.include?(nil)
50 50 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
51 51 content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
52 52 end
53 53
54 54 # Renders the projects index
55 55 def render_project_hierarchy(projects)
56 56 render_project_nested_lists(projects) do |project|
57 57 s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
58 58 if project.description.present?
59 59 s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
60 60 end
61 61 s
62 62 end
63 63 end
64 64
65 65 # Returns a set of options for a select field, grouped by project.
66 66 def version_options_for_select(versions, selected=nil)
67 67 grouped = Hash.new {|h,k| h[k] = []}
68 68 versions.each do |version|
69 69 grouped[version.project.name] << [version.name, version.id]
70 70 end
71 71
72 72 if grouped.keys.size > 1
73 73 grouped_options_for_select(grouped, selected && selected.id)
74 74 else
75 75 options_for_select((grouped.values.first || []), selected && selected.id)
76 76 end
77 77 end
78 78
79 79 def format_version_sharing(sharing)
80 80 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
81 81 l("label_version_sharing_#{sharing}")
82 82 end
83 83 end
@@ -1,162 +1,162
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 def filters_options_for_select(query)
22 22 options_for_select(filters_options(query))
23 23 end
24 24
25 25 def filters_options(query)
26 26 options = [[]]
27 27 sorted_options = query.available_filters.sort do |a, b|
28 28 ord = 0
29 29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 30 ord = a[1][:order] <=> b[1][:order]
31 31 else
32 32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 34 if cn != 0
35 35 ord = cn
36 36 else
37 37 f = (a[1][:field] <=> b[1][:field])
38 38 if f != 0
39 39 ord = f
40 40 else
41 41 # assigned_to or author
42 42 ord = (a[0] <=> b[0])
43 43 end
44 44 end
45 45 end
46 46 ord
47 47 end
48 48 options += sorted_options.map do |field, field_options|
49 49 [field_options[:name], field]
50 50 end
51 51 end
52 52
53 53 def available_block_columns_tags(query)
54 54 tags = ''.html_safe
55 55 query.available_block_columns.each do |column|
56 56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 57 end
58 58 tags
59 59 end
60 60
61 61 def column_header(column)
62 62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
63 63 :default_order => column.default_order) :
64 64 content_tag('th', h(column.caption))
65 65 end
66 66
67 67 def column_content(column, issue)
68 68 value = column.value(issue)
69 69 if value.is_a?(Array)
70 70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
71 71 else
72 72 column_value(column, issue, value)
73 73 end
74 74 end
75 75
76 76 def column_value(column, issue, value)
77 77 case value.class.name
78 78 when 'String'
79 79 if column.name == :subject
80 80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 81 elsif column.name == :description
82 82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
83 83 else
84 84 h(value)
85 85 end
86 86 when 'Time'
87 87 format_time(value)
88 88 when 'Date'
89 89 format_date(value)
90 90 when 'Fixnum', 'Float'
91 91 if column.name == :done_ratio
92 92 progress_bar(value, :width => '80px')
93 93 elsif column.name == :spent_hours
94 94 sprintf "%.2f", value
95 95 elsif column.name == :hours
96 96 html_hours("%.2f" % value)
97 97 else
98 98 h(value.to_s)
99 99 end
100 100 when 'User'
101 101 link_to_user value
102 102 when 'Project'
103 103 link_to_project value
104 104 when 'Version'
105 105 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
106 106 when 'TrueClass'
107 107 l(:general_text_Yes)
108 108 when 'FalseClass'
109 109 l(:general_text_No)
110 110 when 'Issue'
111 111 value.visible? ? link_to_issue(value) : "##{value.id}"
112 112 when 'IssueRelation'
113 113 other = value.other_issue(issue)
114 114 content_tag('span',
115 115 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
116 116 :class => value.css_classes_for(issue))
117 117 else
118 118 h(value)
119 119 end
120 120 end
121 121
122 122 # Retrieve query from session or build a new query
123 123 def retrieve_query
124 124 if !params[:query_id].blank?
125 125 cond = "project_id IS NULL"
126 126 cond << " OR project_id = #{@project.id}" if @project
127 127 @query = IssueQuery.find(params[:query_id], :conditions => cond)
128 128 raise ::Unauthorized unless @query.visible?
129 129 @query.project = @project
130 130 session[:query] = {:id => @query.id, :project_id => @query.project_id}
131 131 sort_clear
132 132 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
133 133 # Give it a name, required to be valid
134 134 @query = IssueQuery.new(:name => "_")
135 135 @query.project = @project
136 136 @query.build_from_params(params)
137 137 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
138 138 else
139 139 # retrieve from session
140 140 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
141 141 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
142 142 @query.project = @project
143 143 end
144 144 end
145 145
146 146 def retrieve_query_from_session
147 147 if session[:query]
148 148 if session[:query][:id]
149 149 @query = IssueQuery.find_by_id(session[:query][:id])
150 150 return unless @query
151 151 else
152 152 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
153 153 end
154 154 if session[:query].has_key?(:project_id)
155 155 @query.project_id = session[:query][:project_id]
156 156 else
157 157 @query.project = @project
158 158 end
159 159 @query
160 160 end
161 161 end
162 162 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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' && row[k] == (v == 0 ? "f" : "t"))
28 28 } unless criteria.nil?
29 29 a = a + row["total"].to_i if match == 1
30 30 } unless data.nil?
31 31 a
32 32 end
33 33
34 34 def aggregate_link(data, criteria, *args)
35 35 a = aggregate data, criteria
36 36 a > 0 ? link_to(h(a), *args) : '-'
37 37 end
38 38
39 39 def aggregate_path(project, field, row, options={})
40 40 parameters = {:set_filter => 1, :subproject_id => '!*', field => row.id}.merge(options)
41 41 project_issues_path(row.is_a?(Project) ? row : project, parameters)
42 42 end
43 43 end
@@ -1,297 +1,297
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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').all.collect do |change|
47 47 case change.action
48 48 when 'A'
49 49 # Detects moved/copied files
50 50 if !change.from_path.blank?
51 51 change.action =
52 52 @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
53 53 end
54 54 change
55 55 when 'D'
56 56 @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
57 57 else
58 58 change
59 59 end
60 60 end.compact
61 61
62 62 tree = { }
63 63 changes.each do |change|
64 64 p = tree
65 65 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
66 66 path = ''
67 67 dirs.each do |dir|
68 68 path += '/' + dir
69 69 p[:s] ||= {}
70 70 p = p[:s]
71 71 p[path] ||= {}
72 72 p = p[path]
73 73 end
74 74 p[:c] = change
75 75 end
76 76 render_changes_tree(tree[:s])
77 77 end
78 78
79 79 def render_changes_tree(tree)
80 80 return '' if tree.nil?
81 81 output = ''
82 82 output << '<ul>'
83 83 tree.keys.sort.each do |file|
84 84 style = 'change'
85 85 text = File.basename(h(file))
86 86 if s = tree[file][:s]
87 87 style << ' folder'
88 88 path_param = to_path_param(@repository.relative_path(file))
89 89 text = link_to(h(text), :controller => 'repositories',
90 90 :action => 'show',
91 91 :id => @project,
92 92 :repository_id => @repository.identifier_param,
93 93 :path => path_param,
94 94 :rev => @changeset.identifier)
95 95 output << "<li class='#{style}'>#{text}"
96 96 output << render_changes_tree(s)
97 97 output << "</li>"
98 98 elsif c = tree[file][:c]
99 99 style << " change-#{c.action}"
100 100 path_param = to_path_param(@repository.relative_path(c.path))
101 101 text = link_to(h(text), :controller => 'repositories',
102 102 :action => 'entry',
103 103 :id => @project,
104 104 :repository_id => @repository.identifier_param,
105 105 :path => path_param,
106 106 :rev => @changeset.identifier) unless c.action == 'D'
107 107 text << " - #{h(c.revision)}" unless c.revision.blank?
108 108 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
109 109 :action => 'diff',
110 110 :id => @project,
111 111 :repository_id => @repository.identifier_param,
112 112 :path => path_param,
113 113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 115 output << "<li class='#{style}'>#{text}</li>"
116 116 end
117 117 end
118 118 output << '</ul>'
119 119 output.html_safe
120 120 end
121 121
122 122 def repository_field_tags(form, repository)
123 123 method = repository.class.name.demodulize.underscore + "_field_tags"
124 124 if repository.is_a?(Repository) &&
125 125 respond_to?(method) && method != 'repository_field_tags'
126 126 send(method, form, repository)
127 127 end
128 128 end
129 129
130 130 def scm_select_tag(repository)
131 131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 132 Redmine::Scm::Base.all.each do |scm|
133 133 if Setting.enabled_scm.include?(scm) ||
134 134 (repository && repository.class.name.demodulize == scm)
135 135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 136 end
137 137 end
138 138 select_tag('repository_scm',
139 139 options_for_select(scm_options, repository.class.name.demodulize),
140 140 :disabled => (repository && !repository.new_record?),
141 141 :data => {:remote => true, :method => 'get'})
142 142 end
143 143
144 144 def with_leading_slash(path)
145 145 path.to_s.starts_with?('/') ? path : "/#{path}"
146 146 end
147 147
148 148 def without_leading_slash(path)
149 149 path.gsub(%r{^/+}, '')
150 150 end
151 151
152 152 def subversion_field_tags(form, repository)
153 153 content_tag('p', form.text_field(:url, :size => 60, :required => true,
154 154 :disabled => !repository.safe_attribute?('url')) +
155 155 '<br />'.html_safe +
156 156 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
157 157 content_tag('p', form.text_field(:login, :size => 30)) +
158 158 content_tag('p', form.password_field(
159 159 :password, :size => 30, :name => 'ignore',
160 160 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
161 161 :onfocus => "this.value=''; this.name='repository[password]';",
162 162 :onchange => "this.name='repository[password]';"))
163 163 end
164 164
165 165 def darcs_field_tags(form, repository)
166 166 content_tag('p', form.text_field(
167 167 :url, :label => l(:field_path_to_repository),
168 168 :size => 60, :required => true,
169 169 :disabled => !repository.safe_attribute?('url'))) +
170 170 content_tag('p', form.select(
171 171 :log_encoding, [nil] + Setting::ENCODINGS,
172 172 :label => l(:field_commit_logs_encoding), :required => true))
173 173 end
174 174
175 175 def mercurial_field_tags(form, repository)
176 176 content_tag('p', form.text_field(
177 177 :url, :label => l(:field_path_to_repository),
178 178 :size => 60, :required => true,
179 179 :disabled => !repository.safe_attribute?('url')
180 180 ) +
181 181 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
182 182 content_tag('p', form.select(
183 183 :path_encoding, [nil] + Setting::ENCODINGS,
184 184 :label => l(:field_scm_path_encoding)
185 185 ) +
186 186 '<br />'.html_safe + l(:text_scm_path_encoding_note))
187 187 end
188 188
189 189 def git_field_tags(form, repository)
190 190 content_tag('p', form.text_field(
191 191 :url, :label => l(:field_path_to_repository),
192 192 :size => 60, :required => true,
193 193 :disabled => !repository.safe_attribute?('url')
194 194 ) +
195 195 '<br />'.html_safe +
196 196 l(:text_git_repository_note)) +
197 197 content_tag('p', form.select(
198 198 :path_encoding, [nil] + Setting::ENCODINGS,
199 199 :label => l(:field_scm_path_encoding)
200 200 ) +
201 201 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
202 202 content_tag('p', form.check_box(
203 203 :extra_report_last_commit,
204 204 :label => l(:label_git_report_last_commit)
205 205 ))
206 206 end
207 207
208 208 def cvs_field_tags(form, repository)
209 209 content_tag('p', form.text_field(
210 210 :root_url,
211 211 :label => l(:field_cvsroot),
212 212 :size => 60, :required => true,
213 213 :disabled => !repository.safe_attribute?('root_url'))) +
214 214 content_tag('p', form.text_field(
215 215 :url,
216 216 :label => l(:field_cvs_module),
217 217 :size => 30, :required => true,
218 218 :disabled => !repository.safe_attribute?('url'))) +
219 219 content_tag('p', form.select(
220 220 :log_encoding, [nil] + Setting::ENCODINGS,
221 221 :label => l(:field_commit_logs_encoding), :required => true)) +
222 222 content_tag('p', form.select(
223 223 :path_encoding, [nil] + Setting::ENCODINGS,
224 224 :label => l(:field_scm_path_encoding)
225 225 ) +
226 226 '<br />'.html_safe + l(:text_scm_path_encoding_note))
227 227 end
228 228
229 229 def bazaar_field_tags(form, repository)
230 230 content_tag('p', form.text_field(
231 231 :url, :label => l(:field_path_to_repository),
232 232 :size => 60, :required => true,
233 233 :disabled => !repository.safe_attribute?('url'))) +
234 234 content_tag('p', form.select(
235 235 :log_encoding, [nil] + Setting::ENCODINGS,
236 236 :label => l(:field_commit_logs_encoding), :required => true))
237 237 end
238 238
239 239 def filesystem_field_tags(form, repository)
240 240 content_tag('p', form.text_field(
241 241 :url, :label => l(:field_root_directory),
242 242 :size => 60, :required => true,
243 243 :disabled => !repository.safe_attribute?('url'))) +
244 244 content_tag('p', form.select(
245 245 :path_encoding, [nil] + Setting::ENCODINGS,
246 246 :label => l(:field_scm_path_encoding)
247 247 ) +
248 248 '<br />'.html_safe + l(:text_scm_path_encoding_note))
249 249 end
250 250
251 251 def index_commits(commits, heads)
252 252 return nil if commits.nil? or commits.first.parents.nil?
253 253 refs_map = {}
254 254 heads.each do |head|
255 255 refs_map[head.scmid] ||= []
256 256 refs_map[head.scmid] << head
257 257 end
258 258 commits_by_scmid = {}
259 259 commits.reverse.each_with_index do |commit, commit_index|
260 260 commits_by_scmid[commit.scmid] = {
261 261 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
262 262 :rdmid => commit_index,
263 263 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
264 264 :scmid => commit.scmid,
265 265 :href => block_given? ? yield(commit.scmid) : commit.scmid
266 266 }
267 267 end
268 268 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
269 269 space = nil
270 270 heads.each do |head|
271 271 if commits_by_scmid.include? head.scmid
272 272 space = index_head((space || -1) + 1, head, commits_by_scmid)
273 273 end
274 274 end
275 275 # when no head matched anything use first commit
276 276 space ||= index_head(0, commits.first, commits_by_scmid)
277 277 return commits_by_scmid, space
278 278 end
279 279
280 280 def index_head(space, commit, commits_by_scmid)
281 281 stack = [[space, commits_by_scmid[commit.scmid]]]
282 282 max_space = space
283 283 until stack.empty?
284 284 space, commit = stack.pop
285 285 commit[:space] = space if commit[:space].nil?
286 286 space -= 1
287 287 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
288 288 parent_commit = commits_by_scmid[parent_scmid]
289 289 if parent_commit and parent_commit[:space].nil?
290 290 stack.unshift [space += 1, parent_commit]
291 291 end
292 292 end
293 293 max_space = space if max_space < space
294 294 end
295 295 max_space
296 296 end
297 297 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RolesHelper
21 21 end
@@ -1,39 +1,39
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module RoutesHelper
21 21
22 22 # Returns the path to project issues or to the cross-project
23 23 # issue list if project is nil
24 24 def _project_issues_path(project, *args)
25 25 if project
26 26 project_issues_path(project, *args)
27 27 else
28 28 issues_path(*args)
29 29 end
30 30 end
31 31
32 32 def _project_calendar_path(project, *args)
33 33 project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
34 34 end
35 35
36 36 def _project_gantt_path(project, *args)
37 37 project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
38 38 end
39 39 end
@@ -1,70 +1,70
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module SearchHelper
21 21 def highlight_tokens(text, tokens)
22 22 return text unless text && tokens && !tokens.empty?
23 23 re_tokens = tokens.collect {|t| Regexp.escape(t)}
24 24 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
25 25 result = ''
26 26 text.split(regexp).each_with_index do |words, i|
27 27 if result.length > 1200
28 28 # maximum length of the preview reached
29 29 result << '...'
30 30 break
31 31 end
32 32 words = words.mb_chars
33 33 if i.even?
34 34 result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
35 35 else
36 36 t = (tokens.index(words.downcase) || 0) % 4
37 37 result << content_tag('span', h(words), :class => "highlight token-#{t}")
38 38 end
39 39 end
40 40 result.html_safe
41 41 end
42 42
43 43 def type_label(t)
44 44 l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
45 45 end
46 46
47 47 def project_select_tag
48 48 options = [[l(:label_project_all), 'all']]
49 49 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
50 50 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
51 51 options << [@project.name, ''] unless @project.nil?
52 52 label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
53 53 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
54 54 end
55 55
56 56 def render_results_by_type(results_by_type)
57 57 links = []
58 58 # Sorts types by results count
59 59 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
60 60 c = results_by_type[t]
61 61 next if c == 0
62 62 text = "#{type_label(t)} (#{c})"
63 63 links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
64 64 :all_words => params[:all_words], :scope => params[:scope], t => 1)
65 65 end
66 66 ('<ul>'.html_safe +
67 67 links.map {|link| content_tag('li', link)}.join(' ').html_safe +
68 68 '</ul>'.html_safe) unless links.empty?
69 69 end
70 70 end
@@ -1,106 +1,106
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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.send(setting).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}")).html_safe : ''
83 83 end
84 84
85 85 # Renders a notification field for a Redmine::Notifiable option
86 86 def notification_field(notifiable)
87 87 return content_tag(:label,
88 88 check_box_tag('settings[notified_events][]',
89 89 notifiable.name,
90 90 Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
91 91 l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
92 92 :class => notifiable.parent.present? ? "parent" : '').html_safe
93 93 end
94 94
95 95 def cross_project_subtasks_options
96 96 options = [
97 97 [:label_disabled, ''],
98 98 [:label_cross_project_system, 'system'],
99 99 [:label_cross_project_tree, 'tree'],
100 100 [:label_cross_project_hierarchy, 'hierarchy'],
101 101 [:label_cross_project_descendants, 'descendants']
102 102 ]
103 103
104 104 options.map {|label, value| [l(label), value.to_s]}
105 105 end
106 106 end
@@ -1,197 +1,197
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module TimelogHelper
21 21 include ApplicationHelper
22 22
23 23 def render_timelog_breadcrumb
24 24 links = []
25 25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
26 26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
27 27 if @issue
28 28 if @issue.visible?
29 29 links << link_to_issue(@issue, :subject => false)
30 30 else
31 31 links << "##{@issue.id}"
32 32 end
33 33 end
34 34 breadcrumb links
35 35 end
36 36
37 37 # Returns a collection of activities for a select field. time_entry
38 38 # is optional and will be used to check if the selected TimeEntryActivity
39 39 # is active.
40 40 def activity_collection_for_select_options(time_entry=nil, project=nil)
41 41 project ||= @project
42 42 if project.nil?
43 43 activities = TimeEntryActivity.shared.active
44 44 else
45 45 activities = project.activities
46 46 end
47 47
48 48 collection = []
49 49 if time_entry && time_entry.activity && !time_entry.activity.active?
50 50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
51 51 else
52 52 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
53 53 end
54 54 activities.each { |a| collection << [a.name, a.id] }
55 55 collection
56 56 end
57 57
58 58 def select_hours(data, criteria, value)
59 59 if value.to_s.empty?
60 60 data.select {|row| row[criteria].blank? }
61 61 else
62 62 data.select {|row| row[criteria].to_s == value.to_s}
63 63 end
64 64 end
65 65
66 66 def sum_hours(data)
67 67 sum = 0
68 68 data.each do |row|
69 69 sum += row['hours'].to_f
70 70 end
71 71 sum
72 72 end
73 73
74 74 def options_for_period_select(value)
75 75 options_for_select([[l(:label_all_time), 'all'],
76 76 [l(:label_today), 'today'],
77 77 [l(:label_yesterday), 'yesterday'],
78 78 [l(:label_this_week), 'current_week'],
79 79 [l(:label_last_week), 'last_week'],
80 80 [l(:label_last_n_weeks, 2), 'last_2_weeks'],
81 81 [l(:label_last_n_days, 7), '7_days'],
82 82 [l(:label_this_month), 'current_month'],
83 83 [l(:label_last_month), 'last_month'],
84 84 [l(:label_last_n_days, 30), '30_days'],
85 85 [l(:label_this_year), 'current_year']],
86 86 value)
87 87 end
88 88
89 89 def entries_to_csv(entries)
90 90 decimal_separator = l(:general_csv_decimal_separator)
91 91 custom_fields = TimeEntryCustomField.all
92 92 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
93 93 # csv header fields
94 94 headers = [l(:field_spent_on),
95 95 l(:field_user),
96 96 l(:field_activity),
97 97 l(:field_project),
98 98 l(:field_issue),
99 99 l(:field_tracker),
100 100 l(:field_subject),
101 101 l(:field_hours),
102 102 l(:field_comments)
103 103 ]
104 104 # Export custom fields
105 105 headers += custom_fields.collect(&:name)
106 106
107 107 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
108 108 c.to_s,
109 109 l(:general_csv_encoding) ) }
110 110 # csv lines
111 111 entries.each do |entry|
112 112 fields = [format_date(entry.spent_on),
113 113 entry.user,
114 114 entry.activity,
115 115 entry.project,
116 116 (entry.issue ? entry.issue.id : nil),
117 117 (entry.issue ? entry.issue.tracker : nil),
118 118 (entry.issue ? entry.issue.subject : nil),
119 119 entry.hours.to_s.gsub('.', decimal_separator),
120 120 entry.comments
121 121 ]
122 122 fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) }
123 123
124 124 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
125 125 c.to_s,
126 126 l(:general_csv_encoding) ) }
127 127 end
128 128 end
129 129 export
130 130 end
131 131
132 132 def format_criteria_value(criteria_options, value)
133 133 if value.blank?
134 134 "[#{l(:label_none)}]"
135 135 elsif k = criteria_options[:klass]
136 136 obj = k.find_by_id(value.to_i)
137 137 if obj.is_a?(Issue)
138 138 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
139 139 else
140 140 obj
141 141 end
142 142 else
143 143 format_value(value, criteria_options[:format])
144 144 end
145 145 end
146 146
147 147 def report_to_csv(report)
148 148 decimal_separator = l(:general_csv_decimal_separator)
149 149 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
150 150 # Column headers
151 151 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
152 152 headers += report.periods
153 153 headers << l(:label_total)
154 154 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
155 155 c.to_s,
156 156 l(:general_csv_encoding) ) }
157 157 # Content
158 158 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
159 159 # Total row
160 160 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
161 161 row = [ str_total ] + [''] * (report.criteria.size - 1)
162 162 total = 0
163 163 report.periods.each do |period|
164 164 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
165 165 total += sum
166 166 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
167 167 end
168 168 row << ("%.2f" % total).gsub('.',decimal_separator)
169 169 csv << row
170 170 end
171 171 export
172 172 end
173 173
174 174 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
175 175 decimal_separator = l(:general_csv_decimal_separator)
176 176 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
177 177 hours_for_value = select_hours(hours, criteria[level], value)
178 178 next if hours_for_value.empty?
179 179 row = [''] * level
180 180 row << Redmine::CodesetUtil.from_utf8(
181 181 format_criteria_value(available_criteria[criteria[level]], value).to_s,
182 182 l(:general_csv_encoding) )
183 183 row += [''] * (criteria.length - level - 1)
184 184 total = 0
185 185 periods.each do |period|
186 186 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
187 187 total += sum
188 188 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
189 189 end
190 190 row << ("%.2f" % total).gsub('.',decimal_separator)
191 191 csv << row
192 192 if criteria.length > level + 1
193 193 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
194 194 end
195 195 end
196 196 end
197 197 end
@@ -1,21 +1,21
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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.count(:group => 'status').to_hash
23 23 options_for_select([[l(:label_all), ''],
24 24 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
25 25 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
26 26 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
27 27 end
28 28
29 29 def user_mail_notification_options(user)
30 30 user.valid_notification_options.collect {|o| [l(o.last), o.first]}
31 31 end
32 32
33 33 def change_status_link(user)
34 34 url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
35 35
36 36 if user.locked?
37 37 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
38 38 elsif user.registered?
39 39 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
40 40 elsif user != User.current
41 41 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
42 42 end
43 43 end
44 44
45 45 def user_settings_tabs
46 46 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
47 47 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
48 48 ]
49 49 if Group.all.any?
50 50 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
51 51 end
52 52 tabs
53 53 end
54 54 end
@@ -1,57 +1,57
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module VersionsHelper
21 21
22 22 def version_anchor(version)
23 23 if @project == version.project
24 24 anchor version.name
25 25 else
26 26 anchor "#{version.project.try(:identifier)}-#{version.name}"
27 27 end
28 28 end
29 29
30 30 STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
31 31
32 32 def render_issue_status_by(version, criteria)
33 33 criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
34 34
35 35 h = Hash.new {|k,v| k[v] = [0, 0]}
36 36 begin
37 37 # Total issue count
38 38 Issue.count(:group => criteria,
39 39 :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
40 40 # Open issues count
41 41 Issue.count(:group => criteria,
42 42 :include => :status,
43 43 :conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
44 44 rescue ActiveRecord::RecordNotFound
45 45 # When grouping by an association, Rails throws this exception if there's no result (bug)
46 46 end
47 47 # Sort with nil keys in last position
48 48 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])}}
49 49 max = counts.collect {|c| c[:total]}.max
50 50
51 51 render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
52 52 end
53 53
54 54 def status_by_options_for_select(value)
55 55 options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
56 56 end
57 57 end
@@ -1,75 +1,75
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WatchersHelper
21 21
22 22 def watcher_tag(object, user, options={})
23 23 content_tag("span", watcher_link(object, user), :class => watcher_css(object))
24 24 end
25 25
26 26 def watcher_link(object, user)
27 27 return '' unless user && user.logged? && object.respond_to?('watched_by?')
28 28 watched = object.watched_by?(user)
29 29 url = {:controller => 'watchers',
30 30 :action => (watched ? 'unwatch' : 'watch'),
31 31 :object_type => object.class.to_s.underscore,
32 32 :object_id => object.id}
33 33 link_to((watched ? l(:button_unwatch) : l(:button_watch)), url,
34 34 :remote => true, :method => 'post', :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
35 35
36 36 end
37 37
38 38 # Returns the css class used to identify watch links for a given +object+
39 39 def watcher_css(object)
40 40 "#{object.class.to_s.underscore}-#{object.id}-watcher"
41 41 end
42 42
43 43 # Returns a comma separated list of users watching the given object
44 44 def watchers_list(object)
45 45 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
46 46 content = ''.html_safe
47 47 lis = object.watcher_users.collect do |user|
48 48 s = ''.html_safe
49 49 s << avatar(user, :size => "16").to_s
50 50 s << link_to_user(user, :class => 'user')
51 51 if remove_allowed
52 52 url = {:controller => 'watchers',
53 53 :action => 'destroy',
54 54 :object_type => object.class.to_s.underscore,
55 55 :object_id => object.id,
56 56 :user_id => user}
57 57 s << ' '
58 58 s << link_to(image_tag('delete.png'), url,
59 59 :remote => true, :method => 'post', :style => "vertical-align: middle", :class => "delete")
60 60 end
61 61 content << content_tag('li', s)
62 62 end
63 63 content.present? ? content_tag('ul', content) : content
64 64 end
65 65
66 66 def watchers_checkboxes(object, users, checked=nil)
67 67 users.map do |user|
68 68 c = checked.nil? ? object.watched_by?(user) : checked
69 69 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
70 70 content_tag 'label', "#{tag} #{h(user)}".html_safe,
71 71 :id => "issue_watcher_user_ids_#{user.id}",
72 72 :class => "floating"
73 73 end.join.html_safe
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-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WelcomeHelper
21 21 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WikiHelper
21 21
22 22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 24 s = ''.html_safe
25 25 if pages.has_key?(parent)
26 26 pages[parent].each do |page|
27 27 attrs = "value='#{page.id}'"
28 28 attrs << " selected='selected'" if selected == page
29 29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30 30
31 31 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 33 end
34 34 end
35 35 s
36 36 end
37 37
38 38 def wiki_page_breadcrumb(page)
39 39 breadcrumb(page.ancestors.reverse.collect {|parent|
40 40 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
41 41 })
42 42 end
43 43 end
@@ -1,32 +1,32
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WorkflowsHelper
21 21 def field_required?(field)
22 22 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
23 23 end
24 24
25 25 def field_permission_tag(permissions, status, field)
26 26 name = field.is_a?(CustomField) ? field.id.to_s : field
27 27 options = [["", ""], [l(:label_readonly), "readonly"]]
28 28 options << [l(:label_required), "required"] unless field_required?(field)
29 29
30 30 select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name]))
31 31 end
32 32 end
@@ -1,326 +1,326
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19 require "fileutils"
20 20
21 21 class Attachment < ActiveRecord::Base
22 22 belongs_to :container, :polymorphic => true
23 23 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
24 24
25 25 validates_presence_of :filename, :author
26 26 validates_length_of :filename, :maximum => 255
27 27 validates_length_of :disk_filename, :maximum => 255
28 28 validates_length_of :description, :maximum => 255
29 29 validate :validate_max_file_size
30 30
31 31 acts_as_event :title => :filename,
32 32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
33 33
34 34 acts_as_activity_provider :type => 'files',
35 35 :permission => :view_files,
36 36 :author_key => :author_id,
37 37 :find_options => {:select => "#{Attachment.table_name}.*",
38 38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
39 39 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
40 40
41 41 acts_as_activity_provider :type => 'documents',
42 42 :permission => :view_documents,
43 43 :author_key => :author_id,
44 44 :find_options => {:select => "#{Attachment.table_name}.*",
45 45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
46 46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
47 47
48 48 cattr_accessor :storage_path
49 49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
50 50
51 51 cattr_accessor :thumbnails_storage_path
52 52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
53 53
54 54 before_save :files_to_final_location
55 55 after_destroy :delete_from_disk
56 56
57 57 # Returns an unsaved copy of the attachment
58 58 def copy(attributes=nil)
59 59 copy = self.class.new
60 60 copy.attributes = self.attributes.dup.except("id", "downloads")
61 61 copy.attributes = attributes if attributes
62 62 copy
63 63 end
64 64
65 65 def validate_max_file_size
66 66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
67 67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
68 68 end
69 69 end
70 70
71 71 def file=(incoming_file)
72 72 unless incoming_file.nil?
73 73 @temp_file = incoming_file
74 74 if @temp_file.size > 0
75 75 if @temp_file.respond_to?(:original_filename)
76 76 self.filename = @temp_file.original_filename
77 77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
78 78 end
79 79 if @temp_file.respond_to?(:content_type)
80 80 self.content_type = @temp_file.content_type.to_s.chomp
81 81 end
82 82 if content_type.blank? && filename.present?
83 83 self.content_type = Redmine::MimeType.of(filename)
84 84 end
85 85 self.filesize = @temp_file.size
86 86 end
87 87 end
88 88 end
89 89
90 90 def file
91 91 nil
92 92 end
93 93
94 94 def filename=(arg)
95 95 write_attribute :filename, sanitize_filename(arg.to_s)
96 96 filename
97 97 end
98 98
99 99 # Copies the temporary file to its final location
100 100 # and computes its MD5 hash
101 101 def files_to_final_location
102 102 if @temp_file && (@temp_file.size > 0)
103 103 self.disk_directory = target_directory
104 104 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
105 105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
106 106 path = File.dirname(diskfile)
107 107 unless File.directory?(path)
108 108 FileUtils.mkdir_p(path)
109 109 end
110 110 md5 = Digest::MD5.new
111 111 File.open(diskfile, "wb") do |f|
112 112 if @temp_file.respond_to?(:read)
113 113 buffer = ""
114 114 while (buffer = @temp_file.read(8192))
115 115 f.write(buffer)
116 116 md5.update(buffer)
117 117 end
118 118 else
119 119 f.write(@temp_file)
120 120 md5.update(@temp_file)
121 121 end
122 122 end
123 123 self.digest = md5.hexdigest
124 124 end
125 125 @temp_file = nil
126 126 # Don't save the content type if it's longer than the authorized length
127 127 if self.content_type && self.content_type.length > 255
128 128 self.content_type = nil
129 129 end
130 130 end
131 131
132 132 # Deletes the file from the file system if it's not referenced by other attachments
133 133 def delete_from_disk
134 134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
135 135 delete_from_disk!
136 136 end
137 137 end
138 138
139 139 # Returns file's location on disk
140 140 def diskfile
141 141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
142 142 end
143 143
144 144 def title
145 145 title = filename.to_s
146 146 if description.present?
147 147 title << " (#{description})"
148 148 end
149 149 title
150 150 end
151 151
152 152 def increment_download
153 153 increment!(:downloads)
154 154 end
155 155
156 156 def project
157 157 container.try(:project)
158 158 end
159 159
160 160 def visible?(user=User.current)
161 161 if container_id
162 162 container && container.attachments_visible?(user)
163 163 else
164 164 author == user
165 165 end
166 166 end
167 167
168 168 def deletable?(user=User.current)
169 169 if container_id
170 170 container && container.attachments_deletable?(user)
171 171 else
172 172 author == user
173 173 end
174 174 end
175 175
176 176 def image?
177 177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
178 178 end
179 179
180 180 def thumbnailable?
181 181 image?
182 182 end
183 183
184 184 # Returns the full path the attachment thumbnail, or nil
185 185 # if the thumbnail cannot be generated.
186 186 def thumbnail(options={})
187 187 if thumbnailable? && readable?
188 188 size = options[:size].to_i
189 189 if size > 0
190 190 # Limit the number of thumbnails per image
191 191 size = (size / 50) * 50
192 192 # Maximum thumbnail size
193 193 size = 800 if size > 800
194 194 else
195 195 size = Setting.thumbnails_size.to_i
196 196 end
197 197 size = 100 unless size > 0
198 198 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
199 199
200 200 begin
201 201 Redmine::Thumbnail.generate(self.diskfile, target, size)
202 202 rescue => e
203 203 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
204 204 return nil
205 205 end
206 206 end
207 207 end
208 208
209 209 # Deletes all thumbnails
210 210 def self.clear_thumbnails
211 211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
212 212 File.delete file
213 213 end
214 214 end
215 215
216 216 def is_text?
217 217 Redmine::MimeType.is_type?('text', filename)
218 218 end
219 219
220 220 def is_diff?
221 221 self.filename =~ /\.(patch|diff)$/i
222 222 end
223 223
224 224 # Returns true if the file is readable
225 225 def readable?
226 226 File.readable?(diskfile)
227 227 end
228 228
229 229 # Returns the attachment token
230 230 def token
231 231 "#{id}.#{digest}"
232 232 end
233 233
234 234 # Finds an attachment that matches the given token and that has no container
235 235 def self.find_by_token(token)
236 236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
237 237 attachment_id, attachment_digest = $1, $2
238 238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
239 239 if attachment && attachment.container.nil?
240 240 attachment
241 241 end
242 242 end
243 243 end
244 244
245 245 # Bulk attaches a set of files to an object
246 246 #
247 247 # Returns a Hash of the results:
248 248 # :files => array of the attached files
249 249 # :unsaved => array of the files that could not be attached
250 250 def self.attach_files(obj, attachments)
251 251 result = obj.save_attachments(attachments, User.current)
252 252 obj.attach_saved_attachments
253 253 result
254 254 end
255 255
256 256 def self.latest_attach(attachments, filename)
257 257 attachments.sort_by(&:created_on).reverse.detect {
258 258 |att| att.filename.downcase == filename.downcase
259 259 }
260 260 end
261 261
262 262 def self.prune(age=1.day)
263 263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
264 264 end
265 265
266 266 # Moves an existing attachment to its target directory
267 267 def move_to_target_directory!
268 268 if !new_record? & readable?
269 269 src = diskfile
270 270 self.disk_directory = target_directory
271 271 dest = diskfile
272 272 if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest)
273 273 update_column :disk_directory, disk_directory
274 274 end
275 275 end
276 276 end
277 277
278 278 # Moves existing attachments that are stored at the root of the files
279 279 # directory (ie. created before Redmine 2.3) to their target subdirectories
280 280 def self.move_from_root_to_target_directory
281 281 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
282 282 attachment.move_to_target_directory!
283 283 end
284 284 end
285 285
286 286 private
287 287
288 288 # Physically deletes the file from the file system
289 289 def delete_from_disk!
290 290 if disk_filename.present? && File.exist?(diskfile)
291 291 File.delete(diskfile)
292 292 end
293 293 end
294 294
295 295 def sanitize_filename(value)
296 296 # get only the filename, not the whole path
297 297 just_filename = value.gsub(/^.*(\\|\/)/, '')
298 298
299 299 # Finally, replace invalid characters with underscore
300 300 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
301 301 end
302 302
303 303 # Returns the subdirectory in which the attachment will be saved
304 304 def target_directory
305 305 time = created_on || DateTime.now
306 306 time.strftime("%Y/%m")
307 307 end
308 308
309 309 # Returns an ASCII or hashed filename that do not
310 310 # exists yet in the given subdirectory
311 311 def self.disk_filename(filename, directory=nil)
312 312 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
313 313 ascii = ''
314 314 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
315 315 ascii = filename
316 316 else
317 317 ascii = Digest::MD5.hexdigest(filename)
318 318 # keep the extension if any
319 319 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
320 320 end
321 321 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
322 322 timestamp.succ!
323 323 end
324 324 "#{timestamp}_#{ascii}"
325 325 end
326 326 end
@@ -1,92 +1,92
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 # Generic exception for when the AuthSource can not be reached
19 19 # (eg. can not connect to the LDAP)
20 20 class AuthSourceException < Exception; end
21 21 class AuthSourceTimeoutException < AuthSourceException; end
22 22
23 23 class AuthSource < ActiveRecord::Base
24 24 include Redmine::SubclassFactory
25 25 include Redmine::Ciphering
26 26
27 27 has_many :users
28 28
29 29 validates_presence_of :name
30 30 validates_uniqueness_of :name
31 31 validates_length_of :name, :maximum => 60
32 32
33 33 def authenticate(login, password)
34 34 end
35 35
36 36 def test_connection
37 37 end
38 38
39 39 def auth_method_name
40 40 "Abstract"
41 41 end
42 42
43 43 def account_password
44 44 read_ciphered_attribute(:account_password)
45 45 end
46 46
47 47 def account_password=(arg)
48 48 write_ciphered_attribute(:account_password, arg)
49 49 end
50 50
51 51 def searchable?
52 52 false
53 53 end
54 54
55 55 def self.search(q)
56 56 results = []
57 57 AuthSource.all.each do |source|
58 58 begin
59 59 if source.searchable?
60 60 results += source.search(q)
61 61 end
62 62 rescue AuthSourceException => e
63 63 logger.error "Error while searching users in #{source.name}: #{e.message}"
64 64 end
65 65 end
66 66 results
67 67 end
68 68
69 69 def allow_password_changes?
70 70 self.class.allow_password_changes?
71 71 end
72 72
73 73 # Does this auth source backend allow password changes?
74 74 def self.allow_password_changes?
75 75 false
76 76 end
77 77
78 78 # Try to authenticate a user not yet registered against available sources
79 79 def self.authenticate(login, password)
80 80 AuthSource.where(:onthefly_register => true).all.each do |source|
81 81 begin
82 82 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
83 83 attrs = source.authenticate(login, password)
84 84 rescue => e
85 85 logger.error "Error during authentication: #{e.message}"
86 86 attrs = nil
87 87 end
88 88 return attrs if attrs
89 89 end
90 90 return nil
91 91 end
92 92 end
@@ -1,201 +1,201
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 'iconv'
19 19 require 'net/ldap'
20 20 require 'net/ldap/dn'
21 21 require 'timeout'
22 22
23 23 class AuthSourceLdap < AuthSource
24 24 validates_presence_of :host, :port, :attr_login
25 25 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
26 26 validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
27 27 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
28 28 validates_numericality_of :port, :only_integer => true
29 29 validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
30 30 validate :validate_filter
31 31
32 32 before_validation :strip_ldap_attributes
33 33
34 34 def initialize(attributes=nil, *args)
35 35 super
36 36 self.port = 389 if self.port == 0
37 37 end
38 38
39 39 def authenticate(login, password)
40 40 return nil if login.blank? || password.blank?
41 41
42 42 with_timeout do
43 43 attrs = get_user_dn(login, password)
44 44 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
45 45 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
46 46 return attrs.except(:dn)
47 47 end
48 48 end
49 49 rescue Net::LDAP::LdapError => e
50 50 raise AuthSourceException.new(e.message)
51 51 end
52 52
53 53 # test the connection to the LDAP
54 54 def test_connection
55 55 with_timeout do
56 56 ldap_con = initialize_ldap_con(self.account, self.account_password)
57 57 ldap_con.open { }
58 58 end
59 59 rescue Net::LDAP::LdapError => e
60 60 raise AuthSourceException.new(e.message)
61 61 end
62 62
63 63 def auth_method_name
64 64 "LDAP"
65 65 end
66 66
67 67 # Returns true if this source can be searched for users
68 68 def searchable?
69 69 !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
70 70 end
71 71
72 72 # Searches the source for users and returns an array of results
73 73 def search(q)
74 74 q = q.to_s.strip
75 75 return [] unless searchable? && q.present?
76 76
77 77 results = []
78 78 search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
79 79 ldap_con = initialize_ldap_con(self.account, self.account_password)
80 80 ldap_con.search(:base => self.base_dn,
81 81 :filter => search_filter,
82 82 :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
83 83 :size => 10) do |entry|
84 84 attrs = get_user_attributes_from_ldap_entry(entry)
85 85 attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
86 86 results << attrs
87 87 end
88 88 results
89 89 rescue Net::LDAP::LdapError => e
90 90 raise AuthSourceException.new(e.message)
91 91 end
92 92
93 93 private
94 94
95 95 def with_timeout(&block)
96 96 timeout = self.timeout
97 97 timeout = 20 unless timeout && timeout > 0
98 98 Timeout.timeout(timeout) do
99 99 return yield
100 100 end
101 101 rescue Timeout::Error => e
102 102 raise AuthSourceTimeoutException.new(e.message)
103 103 end
104 104
105 105 def ldap_filter
106 106 if filter.present?
107 107 Net::LDAP::Filter.construct(filter)
108 108 end
109 109 rescue Net::LDAP::LdapError
110 110 nil
111 111 end
112 112
113 113 def base_filter
114 114 filter = Net::LDAP::Filter.eq("objectClass", "*")
115 115 if f = ldap_filter
116 116 filter = filter & f
117 117 end
118 118 filter
119 119 end
120 120
121 121 def validate_filter
122 122 if filter.present? && ldap_filter.nil?
123 123 errors.add(:filter, :invalid)
124 124 end
125 125 end
126 126
127 127 def strip_ldap_attributes
128 128 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
129 129 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
130 130 end
131 131 end
132 132
133 133 def initialize_ldap_con(ldap_user, ldap_password)
134 134 options = { :host => self.host,
135 135 :port => self.port,
136 136 :encryption => (self.tls ? :simple_tls : nil)
137 137 }
138 138 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
139 139 Net::LDAP.new options
140 140 end
141 141
142 142 def get_user_attributes_from_ldap_entry(entry)
143 143 {
144 144 :dn => entry.dn,
145 145 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
146 146 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
147 147 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
148 148 :auth_source_id => self.id
149 149 }
150 150 end
151 151
152 152 # Return the attributes needed for the LDAP search. It will only
153 153 # include the user attributes if on-the-fly registration is enabled
154 154 def search_attributes
155 155 if onthefly_register?
156 156 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
157 157 else
158 158 ['dn']
159 159 end
160 160 end
161 161
162 162 # Check if a DN (user record) authenticates with the password
163 163 def authenticate_dn(dn, password)
164 164 if dn.present? && password.present?
165 165 initialize_ldap_con(dn, password).bind
166 166 end
167 167 end
168 168
169 169 # Get the user's dn and any attributes for them, given their login
170 170 def get_user_dn(login, password)
171 171 ldap_con = nil
172 172 if self.account && self.account.include?("$login")
173 173 ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
174 174 else
175 175 ldap_con = initialize_ldap_con(self.account, self.account_password)
176 176 end
177 177 attrs = {}
178 178 search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
179 179
180 180 ldap_con.search( :base => self.base_dn,
181 181 :filter => search_filter,
182 182 :attributes=> search_attributes) do |entry|
183 183
184 184 if onthefly_register?
185 185 attrs = get_user_attributes_from_ldap_entry(entry)
186 186 else
187 187 attrs = {:dn => entry.dn}
188 188 end
189 189
190 190 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
191 191 end
192 192
193 193 attrs
194 194 end
195 195
196 196 def self.get_attr(entry, attr_name)
197 197 if !attr_name.blank?
198 198 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
199 199 end
200 200 end
201 201 end
@@ -1,90 +1,90
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Board < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
22 22 has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
23 23 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
24 24 acts_as_tree :dependent => :nullify
25 25 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
26 26 acts_as_watchable
27 27
28 28 validates_presence_of :name, :description
29 29 validates_length_of :name, :maximum => 30
30 30 validates_length_of :description, :maximum => 255
31 31 validate :validate_board
32 32
33 33 scope :visible, lambda {|*args|
34 34 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
35 35 }
36 36
37 37 safe_attributes 'name', 'description', 'parent_id', 'move_to'
38 38
39 39 def visible?(user=User.current)
40 40 !user.nil? && user.allowed_to?(:view_messages, project)
41 41 end
42 42
43 43 def reload(*args)
44 44 @valid_parents = nil
45 45 super
46 46 end
47 47
48 48 def to_s
49 49 name
50 50 end
51 51
52 52 def valid_parents
53 53 @valid_parents ||= project.boards - self_and_descendants
54 54 end
55 55
56 56 def reset_counters!
57 57 self.class.reset_counters!(id)
58 58 end
59 59
60 60 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
61 61 def self.reset_counters!(board_id)
62 62 board_id = board_id.to_i
63 63 update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
64 64 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
65 65 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
66 66 ["id = ?", board_id])
67 67 end
68 68
69 69 def self.board_tree(boards, parent_id=nil, level=0)
70 70 tree = []
71 71 boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
72 72 tree << [board, level]
73 73 tree += board_tree(boards, board.id, level+1)
74 74 end
75 75 if block_given?
76 76 tree.each do |board, level|
77 77 yield board, level
78 78 end
79 79 end
80 80 tree
81 81 end
82 82
83 83 protected
84 84
85 85 def validate_board
86 86 if parent_id && parent_id_changed?
87 87 errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
88 88 end
89 89 end
90 90 end
@@ -1,37 +1,37
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Change < ActiveRecord::Base
19 19 belongs_to :changeset
20 20
21 21 validates_presence_of :changeset_id, :action, :path
22 22 before_save :init_path
23 23 before_validation :replace_invalid_utf8_of_path
24 24
25 25 def relative_path
26 26 changeset.repository.relative_path(path)
27 27 end
28 28
29 29 def replace_invalid_utf8_of_path
30 30 self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
31 31 self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
32 32 end
33 33
34 34 def init_path
35 35 self.path ||= ""
36 36 end
37 37 end
@@ -1,280 +1,280
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 'iconv'
19 19
20 20 class Changeset < ActiveRecord::Base
21 21 belongs_to :repository
22 22 belongs_to :user
23 23 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
24 24 has_and_belongs_to_many :issues
25 25 has_and_belongs_to_many :parents,
26 26 :class_name => "Changeset",
27 27 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
28 28 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
29 29 has_and_belongs_to_many :children,
30 30 :class_name => "Changeset",
31 31 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
32 32 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
33 33
34 34 acts_as_event :title => Proc.new {|o| o.title},
35 35 :description => :long_comments,
36 36 :datetime => :committed_on,
37 37 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
38 38
39 39 acts_as_searchable :columns => 'comments',
40 40 :include => {:repository => :project},
41 41 :project_key => "#{Repository.table_name}.project_id",
42 42 :date_column => 'committed_on'
43 43
44 44 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
45 45 :author_key => :user_id,
46 46 :find_options => {:include => [:user, {:repository => :project}]}
47 47
48 48 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
49 49 validates_uniqueness_of :revision, :scope => :repository_id
50 50 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
51 51
52 52 scope :visible, lambda {|*args|
53 53 includes(:repository => :project).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_fix_keywords.downcase.split(",").collect(&:strip)
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], 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
136 136 referenced_issues << issue
137 137 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
138 138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
139 139 end
140 140 end
141 141 end
142 142
143 143 referenced_issues.uniq!
144 144 self.issues = referenced_issues unless referenced_issues.empty?
145 145 end
146 146
147 147 def short_comments
148 148 @short_comments || split_comments.first
149 149 end
150 150
151 151 def long_comments
152 152 @long_comments || split_comments.last
153 153 end
154 154
155 155 def text_tag(ref_project=nil)
156 156 tag = if scmid?
157 157 "commit:#{scmid}"
158 158 else
159 159 "r#{revision}"
160 160 end
161 161 if repository && repository.identifier.present?
162 162 tag = "#{repository.identifier}|#{tag}"
163 163 end
164 164 if ref_project && project && ref_project != project
165 165 tag = "#{project.identifier}:#{tag}"
166 166 end
167 167 tag
168 168 end
169 169
170 170 # Returns the title used for the changeset in the activity/search results
171 171 def title
172 172 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
173 173 comm = short_comments.blank? ? '' : (': ' + short_comments)
174 174 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
175 175 end
176 176
177 177 # Returns the previous changeset
178 178 def previous
179 179 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
180 180 end
181 181
182 182 # Returns the next changeset
183 183 def next
184 184 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
185 185 end
186 186
187 187 # Creates a new Change from it's common parameters
188 188 def create_change(change)
189 189 Change.create(:changeset => self,
190 190 :action => change[:action],
191 191 :path => change[:path],
192 192 :from_path => change[:from_path],
193 193 :from_revision => change[:from_revision])
194 194 end
195 195
196 196 # Finds an issue that can be referenced by the commit message
197 197 def find_referenced_issue_by_id(id)
198 198 return nil if id.blank?
199 199 issue = Issue.find_by_id(id.to_i, :include => :project)
200 200 if Setting.commit_cross_project_ref?
201 201 # all issues can be referenced/fixed
202 202 elsif issue
203 203 # issue that belong to the repository project, a subproject or a parent project only
204 204 unless issue.project &&
205 205 (project == issue.project || project.is_ancestor_of?(issue.project) ||
206 206 project.is_descendant_of?(issue.project))
207 207 issue = nil
208 208 end
209 209 end
210 210 issue
211 211 end
212 212
213 213 private
214 214
215 215 def fix_issue(issue)
216 216 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
217 217 if status.nil?
218 218 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
219 219 return issue
220 220 end
221 221
222 222 # the issue may have been updated by the closure of another one (eg. duplicate)
223 223 issue.reload
224 224 # don't change the status is the issue is closed
225 225 return if issue.status && issue.status.is_closed?
226 226
227 227 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
228 228 issue.status = status
229 229 unless Setting.commit_fix_done_ratio.blank?
230 230 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
231 231 end
232 232 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
233 233 { :changeset => self, :issue => issue })
234 234 unless issue.save
235 235 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
236 236 end
237 237 issue
238 238 end
239 239
240 240 def log_time(issue, hours)
241 241 time_entry = TimeEntry.new(
242 242 :user => user,
243 243 :hours => hours,
244 244 :issue => issue,
245 245 :spent_on => commit_date,
246 246 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
247 247 :locale => Setting.default_language)
248 248 )
249 249 time_entry.activity = log_time_activity unless log_time_activity.nil?
250 250
251 251 unless time_entry.save
252 252 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
253 253 end
254 254 time_entry
255 255 end
256 256
257 257 def log_time_activity
258 258 if Setting.commit_logtime_activity_id.to_i > 0
259 259 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
260 260 end
261 261 end
262 262
263 263 def split_comments
264 264 comments =~ /\A(.+?)\r?\n(.*)$/m
265 265 @short_comments = $1 || comments
266 266 @long_comments = $2.to_s.strip
267 267 return @short_comments, @long_comments
268 268 end
269 269
270 270 public
271 271
272 272 # Strips and reencodes a commit log before insertion into the database
273 273 def self.normalize_comments(str, encoding)
274 274 Changeset.to_utf8(str.to_s.strip, encoding)
275 275 end
276 276
277 277 def self.to_utf8(str, encoding)
278 278 Redmine::CodesetUtil.to_utf8(str, encoding)
279 279 end
280 280 end
@@ -1,26 +1,26
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Comment < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :commented, :polymorphic => true, :counter_cache => true
21 21 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 22
23 23 validates_presence_of :commented, :author, :comments
24 24
25 25 safe_attributes 'comments'
26 26 end
@@ -1,24 +1,24
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 CommentObserver < ActiveRecord::Observer
19 19 def after_create(comment)
20 20 if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
21 21 Mailer.news_comment_added(comment).deliver
22 22 end
23 23 end
24 24 end
@@ -1,355 +1,355
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 acts_as_list :scope => 'type = \'#{self.class}\''
23 23 serialize :possible_values
24 24
25 25 validates_presence_of :name, :field_format
26 26 validates_uniqueness_of :name, :scope => :type
27 27 validates_length_of :name, :maximum => 30
28 28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29 29
30 30 validate :validate_custom_field
31 31 before_validation :set_searchable
32 32 after_save :handle_multiplicity_change
33 33
34 34 scope :sorted, lambda { order("#{table_name}.position ASC") }
35 35
36 36 CUSTOM_FIELDS_TABS = [
37 37 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
38 38 :label => :label_issue_plural},
39 39 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
40 40 :label => :label_spent_time},
41 41 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
42 42 :label => :label_project_plural},
43 43 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
44 44 :label => :label_version_plural},
45 45 {:name => 'UserCustomField', :partial => 'custom_fields/index',
46 46 :label => :label_user_plural},
47 47 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
48 48 :label => :label_group_plural},
49 49 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
50 50 :label => TimeEntryActivity::OptionName},
51 51 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
52 52 :label => IssuePriority::OptionName},
53 53 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
54 54 :label => DocumentCategory::OptionName}
55 55 ]
56 56
57 57 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
58 58
59 59 def field_format=(arg)
60 60 # cannot change format of a saved custom field
61 61 super if new_record?
62 62 end
63 63
64 64 def set_searchable
65 65 # make sure these fields are not searchable
66 66 self.searchable = false if %w(int float date bool).include?(field_format)
67 67 # make sure only these fields can have multiple values
68 68 self.multiple = false unless %w(list user version).include?(field_format)
69 69 true
70 70 end
71 71
72 72 def validate_custom_field
73 73 if self.field_format == "list"
74 74 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
75 75 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
76 76 end
77 77
78 78 if regexp.present?
79 79 begin
80 80 Regexp.new(regexp)
81 81 rescue
82 82 errors.add(:regexp, :invalid)
83 83 end
84 84 end
85 85
86 86 if default_value.present? && !valid_field_value?(default_value)
87 87 errors.add(:default_value, :invalid)
88 88 end
89 89 end
90 90
91 91 def possible_values_options(obj=nil)
92 92 case field_format
93 93 when 'user', 'version'
94 94 if obj.respond_to?(:project) && obj.project
95 95 case field_format
96 96 when 'user'
97 97 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
98 98 when 'version'
99 99 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
100 100 end
101 101 elsif obj.is_a?(Array)
102 102 obj.collect {|o| possible_values_options(o)}.reduce(:&)
103 103 else
104 104 []
105 105 end
106 106 when 'bool'
107 107 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
108 108 else
109 109 possible_values || []
110 110 end
111 111 end
112 112
113 113 def possible_values(obj=nil)
114 114 case field_format
115 115 when 'user', 'version'
116 116 possible_values_options(obj).collect(&:last)
117 117 when 'bool'
118 118 ['1', '0']
119 119 else
120 120 values = super()
121 121 if values.is_a?(Array)
122 122 values.each do |value|
123 123 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
124 124 end
125 125 end
126 126 values || []
127 127 end
128 128 end
129 129
130 130 # Makes possible_values accept a multiline string
131 131 def possible_values=(arg)
132 132 if arg.is_a?(Array)
133 133 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
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 casted = nil
141 141 unless value.blank?
142 142 case field_format
143 143 when 'string', 'text', 'list'
144 144 casted = value
145 145 when 'date'
146 146 casted = begin; value.to_date; rescue; nil end
147 147 when 'bool'
148 148 casted = (value == '1' ? true : false)
149 149 when 'int'
150 150 casted = value.to_i
151 151 when 'float'
152 152 casted = value.to_f
153 153 when 'user', 'version'
154 154 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
155 155 end
156 156 end
157 157 casted
158 158 end
159 159
160 160 def value_from_keyword(keyword, customized)
161 161 possible_values_options = possible_values_options(customized)
162 162 if possible_values_options.present?
163 163 keyword = keyword.to_s.downcase
164 164 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
165 165 if v.is_a?(Array)
166 166 v.last
167 167 else
168 168 v
169 169 end
170 170 end
171 171 else
172 172 keyword
173 173 end
174 174 end
175 175
176 176 # Returns a ORDER BY clause that can used to sort customized
177 177 # objects by their value of the custom field.
178 178 # Returns nil if the custom field can not be used for sorting.
179 179 def order_statement
180 180 return nil if multiple?
181 181 case field_format
182 182 when 'string', 'text', 'list', 'date', 'bool'
183 183 # COALESCE is here to make sure that blank and NULL values are sorted equally
184 184 "COALESCE(#{join_alias}.value, '')"
185 185 when 'int', 'float'
186 186 # Make the database cast values into numeric
187 187 # Postgresql will raise an error if a value can not be casted!
188 188 # CustomValue validations should ensure that it doesn't occur
189 189 "CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))"
190 190 when 'user', 'version'
191 191 value_class.fields_for_order_statement(value_join_alias)
192 192 else
193 193 nil
194 194 end
195 195 end
196 196
197 197 # Returns a GROUP BY clause that can used to group by custom value
198 198 # Returns nil if the custom field can not be used for grouping.
199 199 def group_statement
200 200 return nil if multiple?
201 201 case field_format
202 202 when 'list', 'date', 'bool', 'int'
203 203 order_statement
204 204 when 'user', 'version'
205 205 "COALESCE(#{join_alias}.value, '')"
206 206 else
207 207 nil
208 208 end
209 209 end
210 210
211 211 def join_for_order_statement
212 212 case field_format
213 213 when 'user', 'version'
214 214 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
215 215 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
216 216 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
217 217 " AND #{join_alias}.custom_field_id = #{id}" +
218 218 " AND #{join_alias}.value <> ''" +
219 219 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
220 220 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
221 221 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
222 222 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
223 223 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
224 224 " ON CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,0)) = #{value_join_alias}.id"
225 225 when 'int', 'float'
226 226 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
227 227 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
228 228 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
229 229 " AND #{join_alias}.custom_field_id = #{id}" +
230 230 " AND #{join_alias}.value <> ''" +
231 231 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
232 232 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
233 233 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
234 234 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
235 235 when 'string', 'text', 'list', 'date', 'bool'
236 236 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
237 237 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
238 238 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
239 239 " AND #{join_alias}.custom_field_id = #{id}" +
240 240 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
241 241 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
242 242 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
243 243 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
244 244 else
245 245 nil
246 246 end
247 247 end
248 248
249 249 def join_alias
250 250 "cf_#{id}"
251 251 end
252 252
253 253 def value_join_alias
254 254 join_alias + "_" + field_format
255 255 end
256 256
257 257 def <=>(field)
258 258 position <=> field.position
259 259 end
260 260
261 261 # Returns the class that values represent
262 262 def value_class
263 263 case field_format
264 264 when 'user', 'version'
265 265 field_format.classify.constantize
266 266 else
267 267 nil
268 268 end
269 269 end
270 270
271 271 def self.customized_class
272 272 self.name =~ /^(.+)CustomField$/
273 273 begin; $1.constantize; rescue nil; end
274 274 end
275 275
276 276 # to move in project_custom_field
277 277 def self.for_all
278 278 where(:is_for_all => true).order('position').all
279 279 end
280 280
281 281 def type_name
282 282 nil
283 283 end
284 284
285 285 # Returns the error messages for the given value
286 286 # or an empty array if value is a valid value for the custom field
287 287 def validate_field_value(value)
288 288 errs = []
289 289 if value.is_a?(Array)
290 290 if !multiple?
291 291 errs << ::I18n.t('activerecord.errors.messages.invalid')
292 292 end
293 293 if is_required? && value.detect(&:present?).nil?
294 294 errs << ::I18n.t('activerecord.errors.messages.blank')
295 295 end
296 296 value.each {|v| errs += validate_field_value_format(v)}
297 297 else
298 298 if is_required? && value.blank?
299 299 errs << ::I18n.t('activerecord.errors.messages.blank')
300 300 end
301 301 errs += validate_field_value_format(value)
302 302 end
303 303 errs
304 304 end
305 305
306 306 # Returns true if value is a valid value for the custom field
307 307 def valid_field_value?(value)
308 308 validate_field_value(value).empty?
309 309 end
310 310
311 311 def format_in?(*args)
312 312 args.include?(field_format)
313 313 end
314 314
315 315 protected
316 316
317 317 # Returns the error message for the given value regarding its format
318 318 def validate_field_value_format(value)
319 319 errs = []
320 320 if value.present?
321 321 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
322 322 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
323 323 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
324 324
325 325 # Format specific validations
326 326 case field_format
327 327 when 'int'
328 328 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
329 329 when 'float'
330 330 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
331 331 when 'date'
332 332 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
333 333 when 'list'
334 334 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
335 335 end
336 336 end
337 337 errs
338 338 end
339 339
340 340 # Removes multiple values for the custom field after setting the multiple attribute to false
341 341 # We kepp the value with the highest id for each customized object
342 342 def handle_multiplicity_change
343 343 if !new_record? && multiple_was && !multiple
344 344 ids = custom_values.
345 345 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
346 346 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
347 347 " AND cve.id > #{CustomValue.table_name}.id)").
348 348 pluck(:id)
349 349
350 350 if ids.any?
351 351 custom_values.where(:id => ids).delete_all
352 352 end
353 353 end
354 354 end
355 355 end
@@ -1,50 +1,50
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
20 20
21 21 def custom_field_id
22 22 custom_field.id
23 23 end
24 24
25 25 def true?
26 26 self.value == '1'
27 27 end
28 28
29 29 def editable?
30 30 custom_field.editable?
31 31 end
32 32
33 33 def visible?
34 34 custom_field.visible?
35 35 end
36 36
37 37 def required?
38 38 custom_field.is_required?
39 39 end
40 40
41 41 def to_s
42 42 value.to_s
43 43 end
44 44
45 45 def validate_value
46 46 custom_field.validate_field_value(value).each do |message|
47 47 customized.errors.add(:base, custom_field.name + ' ' + message)
48 48 end
49 49 end
50 50 end
@@ -1,49 +1,49
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomValue < ActiveRecord::Base
19 19 belongs_to :custom_field
20 20 belongs_to :customized, :polymorphic => true
21 21
22 22 def initialize(attributes=nil, *args)
23 23 super
24 24 if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
25 25 self.value ||= custom_field.default_value
26 26 end
27 27 end
28 28
29 29 # Returns true if the boolean custom value is true
30 30 def true?
31 31 self.value == '1'
32 32 end
33 33
34 34 def editable?
35 35 custom_field.editable?
36 36 end
37 37
38 38 def visible?
39 39 custom_field.visible?
40 40 end
41 41
42 42 def required?
43 43 custom_field.is_required?
44 44 end
45 45
46 46 def to_s
47 47 value.to_s
48 48 end
49 49 end
@@ -1,57 +1,57
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Document < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
22 22 acts_as_attachable :delete_permission => :manage_documents
23 23
24 24 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
25 25 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
26 26 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
27 27 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
28 28 acts_as_activity_provider :find_options => {:include => :project}
29 29
30 30 validates_presence_of :project, :title, :category
31 31 validates_length_of :title, :maximum => 60
32 32
33 33 scope :visible, lambda {|*args|
34 34 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
35 35 }
36 36
37 37 safe_attributes 'category_id', 'title', 'description'
38 38
39 39 def visible?(user=User.current)
40 40 !user.nil? && user.allowed_to?(:view_documents, project)
41 41 end
42 42
43 43 def initialize(attributes=nil, *args)
44 44 super
45 45 if new_record?
46 46 self.category ||= DocumentCategory.default
47 47 end
48 48 end
49 49
50 50 def updated_on
51 51 unless @updated_on
52 52 a = attachments.last
53 53 @updated_on = (a && a.created_on) || created_on
54 54 end
55 55 @updated_on
56 56 end
57 57 end
@@ -1,40 +1,40
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class DocumentCategoryCustomField < CustomField
19 19 def type_name
20 20 :enumeration_doc_categories
21 21 end
22 22 end
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 DocumentObserver < ActiveRecord::Observer
19 19 def after_create(document)
20 20 Mailer.document_added(document).deliver if Setting.notified_events.include?('document_added')
21 21 end
22 22 end
@@ -1,38 +1,38
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EnabledModule < ActiveRecord::Base
19 19 belongs_to :project
20 20
21 21 validates_presence_of :name
22 22 validates_uniqueness_of :name, :scope => :project_id
23 23
24 24 after_create :module_enabled
25 25
26 26 private
27 27
28 28 # after_create callback used to do things when a module is enabled
29 29 def module_enabled
30 30 case name
31 31 when 'wiki'
32 32 # Create a wiki with a default start page
33 33 if project && project.wiki.nil?
34 34 Wiki.create(:project => project, :start_page => 'Wiki')
35 35 end
36 36 end
37 37 end
38 38 end
@@ -1,141 +1,141
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Enumeration < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 default_scope :order => "#{Enumeration.table_name}.position ASC"
22 22
23 23 belongs_to :project
24 24
25 25 acts_as_list :scope => 'type = \'#{type}\''
26 26 acts_as_customizable
27 27 acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
28 28
29 29 before_destroy :check_integrity
30 30 before_save :check_default
31 31
32 32 attr_protected :type
33 33
34 34 validates_presence_of :name
35 35 validates_uniqueness_of :name, :scope => [:type, :project_id]
36 36 validates_length_of :name, :maximum => 30
37 37
38 38 scope :shared, lambda { where(:project_id => nil) }
39 39 scope :sorted, lambda { order("#{table_name}.position ASC") }
40 40 scope :active, lambda { where(:active => true) }
41 41 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
42 42
43 43 def self.default
44 44 # Creates a fake default scope so Enumeration.default will check
45 45 # it's type. STI subclasses will automatically add their own
46 46 # types to the finder.
47 47 if self.descends_from_active_record?
48 48 where(:is_default => true, :type => 'Enumeration').first
49 49 else
50 50 # STI classes are
51 51 where(:is_default => true).first
52 52 end
53 53 end
54 54
55 55 # Overloaded on concrete classes
56 56 def option_name
57 57 nil
58 58 end
59 59
60 60 def check_default
61 61 if is_default? && is_default_changed?
62 62 Enumeration.update_all({:is_default => false}, {:type => type})
63 63 end
64 64 end
65 65
66 66 # Overloaded on concrete classes
67 67 def objects_count
68 68 0
69 69 end
70 70
71 71 def in_use?
72 72 self.objects_count != 0
73 73 end
74 74
75 75 # Is this enumeration overiding a system level enumeration?
76 76 def is_override?
77 77 !self.parent.nil?
78 78 end
79 79
80 80 alias :destroy_without_reassign :destroy
81 81
82 82 # Destroy the enumeration
83 83 # If a enumeration is specified, objects are reassigned
84 84 def destroy(reassign_to = nil)
85 85 if reassign_to && reassign_to.is_a?(Enumeration)
86 86 self.transfer_relations(reassign_to)
87 87 end
88 88 destroy_without_reassign
89 89 end
90 90
91 91 def <=>(enumeration)
92 92 position <=> enumeration.position
93 93 end
94 94
95 95 def to_s; name end
96 96
97 97 # Returns the Subclasses of Enumeration. Each Subclass needs to be
98 98 # required in development mode.
99 99 #
100 100 # Note: subclasses is protected in ActiveRecord
101 101 def self.get_subclasses
102 102 subclasses
103 103 end
104 104
105 105 # Does the +new+ Hash override the previous Enumeration?
106 106 def self.overridding_change?(new, previous)
107 107 if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
108 108 return false
109 109 else
110 110 return true
111 111 end
112 112 end
113 113
114 114 # Does the +new+ Hash have the same custom values as the previous Enumeration?
115 115 def self.same_custom_values?(new, previous)
116 116 previous.custom_field_values.each do |custom_value|
117 117 if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
118 118 return false
119 119 end
120 120 end
121 121
122 122 return true
123 123 end
124 124
125 125 # Are the new and previous fields equal?
126 126 def self.same_active_state?(new, previous)
127 127 new = (new == "1" ? true : false)
128 128 return new == previous
129 129 end
130 130
131 131 private
132 132 def check_integrity
133 133 raise "Can't delete enumeration" if self.in_use?
134 134 end
135 135
136 136 end
137 137
138 138 # Force load the subclasses in development mode
139 139 require_dependency 'time_entry_activity'
140 140 require_dependency 'document_category'
141 141 require_dependency 'issue_priority'
@@ -1,89 +1,89
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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, :after_add => :user_added,
22 22 :after_remove => :user_removed
23 23
24 24 acts_as_customizable
25 25
26 26 validates_presence_of :lastname
27 27 validates_uniqueness_of :lastname, :case_sensitive => false
28 28 validates_length_of :lastname, :maximum => 30
29 29
30 30 before_destroy :remove_references_before_destroy
31 31
32 32 scope :sorted, lambda { order("#{table_name}.lastname ASC") }
33 33
34 34 safe_attributes 'name',
35 35 'user_ids',
36 36 'custom_field_values',
37 37 'custom_fields',
38 38 :if => lambda {|group, user| user.admin?}
39 39
40 40 def to_s
41 41 lastname.to_s
42 42 end
43 43
44 44 def name
45 45 lastname
46 46 end
47 47
48 48 def name=(arg)
49 49 self.lastname = arg
50 50 end
51 51
52 52 def user_added(user)
53 53 members.each do |member|
54 54 next if member.project.nil?
55 55 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)
56 56 member.member_roles.each do |member_role|
57 57 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
58 58 end
59 59 user_member.save!
60 60 end
61 61 end
62 62
63 63 def user_removed(user)
64 64 members.each do |member|
65 65 MemberRole.
66 66 includes(:member).
67 67 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
68 68 all.
69 69 each(&:destroy)
70 70 end
71 71 end
72 72
73 73 def self.human_attribute_name(attribute_key_name, *args)
74 74 attr_name = attribute_key_name.to_s
75 75 if attr_name == 'lastname'
76 76 attr_name = "name"
77 77 end
78 78 super(attr_name, *args)
79 79 end
80 80
81 81 private
82 82
83 83 # Removes references that are not handled by associations
84 84 def remove_references_before_destroy
85 85 return if self.id.nil?
86 86
87 87 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
88 88 end
89 89 end
@@ -1,22 +1,22
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class GroupCustomField < CustomField
19 19 def type_name
20 20 :label_group_plural
21 21 end
22 22 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now