Compare commits

..
32 Commits
Author SHA1 Message Date
Jean-Philippe Lang b775baace0 tagged version 0.9.1
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/tags/0.9.1@3350 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-30 11:05:49 +00:00
Jean-Philippe Lang 20a6d7eb86 Merged r3348 from trunk (version set to 0.9.1).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3349 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-30 10:54:45 +00:00
Jean-Philippe Lang b87cf0c91c Merged r3346 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3347 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-30 10:37:05 +00:00
Jean-Philippe Lang 14ee72def2 Merged r3341 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3345 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-30 10:28:26 +00:00
Jean-Philippe Lang a843cfff36 Merged r3343 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3344 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-30 10:26:30 +00:00
Jean-Philippe Lang 3d7cb0f40e Merged r3333 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3334 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-18 18:01:19 +00:00
Jean-Philippe Lang 92b267a608 Merged r3328 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3329 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-17 12:01:33 +00:00
Jean-Philippe Lang c1be8bcf9f Merged r3325 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3326 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-17 11:18:04 +00:00
Jean-Philippe Lang ff9aa9db59 Merged r3323 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3324 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-17 11:09:29 +00:00
Jean-Philippe Lang 65ee523852 Merged r3312 and r3320 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3321 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-17 10:34:47 +00:00
Jean-Philippe Lang 1f5d95b027 Merged r3309 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3318 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-17 10:28:06 +00:00
Jean-Philippe Lang c5a59aff5b Merged r3305, r3306, r3307, r3311 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3317 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-16 13:57:41 +00:00
Eric Davis f60881518a Merged r3315 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3316 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-14 23:33:48 +00:00
Jean-Philippe Lang 1efb25a433 Merged r3298 to r3302 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3303 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-10 10:22:03 +00:00
Jean-Philippe Lang eaaa471d6a Merged r3295 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3296 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 11:50:31 +00:00
Jean-Philippe Lang 2b6d8125d1 Merged r3293 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3294 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 11:47:38 +00:00
Jean-Philippe Lang 12b75ded08 Locales updated to trunk @ r3291.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3292 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 10:41:27 +00:00
Jean-Philippe Lang 46bf2b9276 Merged r3289 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3290 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 10:27:50 +00:00
Jean-Philippe Lang 9d82bff1a8 Merged r3282 to r3284 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3287 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 10:04:21 +00:00
Jean-Philippe Lang 4c75864948 Merged r3258 and r3281 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3286 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-09 10:02:16 +00:00
Jean-Philippe Lang 9d2474c234 Merged r3277 to r3279 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3280 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-03 11:47:25 +00:00
Jean-Philippe Lang 7193817dea Translation updates.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3276 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-01-03 10:32:37 +00:00
Eric Davis 610d7d4ba4 Merged r3267 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3268 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-30 00:51:08 +00:00
Eric Davis 4b41788848 Merged r3265 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3266 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-30 00:26:48 +00:00
Eric Davis e1423c7c23 Merged r3263 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3264 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-29 23:33:46 +00:00
Eric Davis 9c9f6722f6 Merged r3261 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3262 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-29 17:58:21 +00:00
Jean-Philippe Lang a8be47295a Merged r3259 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3260 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-29 14:56:17 +00:00
Eric Davis ba98197637 Merged r3256 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3257 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-29 00:44:28 +00:00
Jean-Philippe Lang a23399f220 Merged r3249 to r3252 and r3254 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3255 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-27 12:41:27 +00:00
Jean-Philippe Lang 718cd596e0 Merged r3246 and r3247 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3248 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-26 13:54:57 +00:00
Jean-Philippe Lang c5ccfede6d Set VERSION::BRANCH to stable.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3245 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-26 10:27:46 +00:00
Jean-Philippe Lang 7702bdcdab 0.9-stable branch created
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3244 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-12-26 10:22:16 +00:00
1099 changed files with 18453 additions and 84944 deletions

No files matched your search

-6
View File
@@ -1,7 +1,4 @@
/.project
/.loadpath
/config/additional_environment.rb /config/additional_environment.rb
/config/configuration.yml
/config/database.yml /config/database.yml
/config/email.yml /config/email.yml
/config/initializers/session_store.rb /config/initializers/session_store.rb
@@ -10,8 +7,6 @@
/db/*.sqlite3 /db/*.sqlite3
/db/schema.rb /db/schema.rb
/files/* /files/*
/lib/redmine/scm/adapters/mercurial/redminehelper.pyc
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
/log/*.log* /log/*.log*
/log/mongrel_debug /log/mongrel_debug
/public/dispatch.* /public/dispatch.*
@@ -22,4 +17,3 @@
/tmp/sockets/* /tmp/sockets/*
/tmp/test/* /tmp/test/*
/vendor/rails /vendor/rails
*.rbc
-29
View File
@@ -1,29 +0,0 @@
syntax: glob
.project
.loadpath
config/additional_environment.rb
config/configuration.yml
config/database.yml
config/email.yml
config/initializers/session_store.rb
coverage
db/*.db
db/*.sqlite3
db/schema.rb
files/*
lib/redmine/scm/adapters/mercurial/redminehelper.pyc
lib/redmine/scm/adapters/mercurial/redminehelper.pyo
log/*.log*
log/mongrel_debug
public/dispatch.*
public/plugin_assets
tmp/*
tmp/cache/*
tmp/sessions/*
tmp/sockets/*
tmp/test/*
vendor/rails
*.rbc
.svn/
.git/
+1 -1
View File
@@ -2,4 +2,4 @@
Redmine is a flexible project management web application written using Ruby on Rails framework. Redmine is a flexible project management web application written using Ruby on Rails framework.
More details can be found at in the doc directory or on the official website http://www.redmine.org More details can be found at http://www.redmine.org
+19 -40
View File
@@ -25,15 +25,23 @@ class AccountController < ApplicationController
# Login request and validation # Login request and validation
def login def login
if request.get? if request.get?
logout_user # Logout user
self.logged_user = nil
else else
authenticate_user # Authenticate user
if Setting.openid? && using_open_id?
open_id_authenticate(params[:openid_url])
else
password_authentication
end
end end
end end
# Log out current user and redirect to welcome page # Log out current user and redirect to welcome page
def logout def logout
logout_user cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
self.logged_user = nil
redirect_to home_url redirect_to home_url
end end
@@ -83,9 +91,9 @@ class AccountController < ApplicationController
else else
@user = User.new(params[:user]) @user = User.new(params[:user])
@user.admin = false @user.admin = false
@user.register @user.status = User::STATUS_REGISTERED
if session[:auth_source_registration] if session[:auth_source_registration]
@user.activate @user.status = User::STATUS_ACTIVE
@user.login = session[:auth_source_registration][:login] @user.login = session[:auth_source_registration][:login]
@user.auth_source_id = session[:auth_source_registration][:auth_source_id] @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
if @user.save if @user.save
@@ -116,8 +124,8 @@ class AccountController < ApplicationController
token = Token.find_by_action_and_value('register', params[:token]) token = Token.find_by_action_and_value('register', params[:token])
redirect_to(home_url) && return unless token and !token.expired? redirect_to(home_url) && return unless token and !token.expired?
user = token.user user = token.user
redirect_to(home_url) && return unless user.registered? redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
user.activate user.status = User::STATUS_ACTIVE
if user.save if user.save
token.destroy token.destroy
flash[:notice] = l(:notice_account_activated) flash[:notice] = l(:notice_account_activated)
@@ -127,22 +135,6 @@ class AccountController < ApplicationController
private private
def logout_user
if User.current.logged?
cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
self.logged_user = nil
end
end
def authenticate_user
if Setting.openid? && using_open_id?
open_id_authenticate(params[:openid_url])
else
password_authentication
end
end
def password_authentication def password_authentication
user = User.try_to_login(params[:username], params[:password]) user = User.try_to_login(params[:username], params[:password])
@@ -170,7 +162,7 @@ class AccountController < ApplicationController
user.mail = registration['email'] unless registration['email'].nil? user.mail = registration['email'] unless registration['email'].nil?
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
user.random_password user.random_password
user.register user.status = User::STATUS_REGISTERED
case Setting.self_registration case Setting.self_registration
when '1' when '1'
@@ -203,25 +195,13 @@ class AccountController < ApplicationController
self.logged_user = user self.logged_user = user
# generate a key and set cookie if autologin # generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin? if params[:autologin] && Setting.autologin?
set_autologin_cookie(user) token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end end
call_hook(:controller_account_success_authentication_after, {:user => user }) call_hook(:controller_account_success_authentication_after, {:user => user })
redirect_back_or_default :controller => 'my', :action => 'page' redirect_back_or_default :controller => 'my', :action => 'page'
end end
def set_autologin_cookie(user)
token = Token.create(:user => user, :action => 'autologin')
cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin'
cookie_options = {
:value => token.value,
:expires => 1.year.from_now,
:path => (Redmine::Configuration['autologin_cookie_path'] || '/'),
:secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
:httponly => true
}
cookies[cookie_name] = cookie_options
end
# Onthefly creation failed, display the registration form to fill/fix attributes # Onthefly creation failed, display the registration form to fill/fix attributes
def onthefly_creation_failed(user, auth_source_options = { }) def onthefly_creation_failed(user, auth_source_options = { })
@user = user @user = user
@@ -230,7 +210,6 @@ class AccountController < ApplicationController
end end
def invalid_credentials def invalid_credentials
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
flash.now[:error] = l(:notice_account_invalid_creditentials) flash.now[:error] = l(:notice_account_invalid_creditentials)
end end
@@ -253,7 +232,7 @@ class AccountController < ApplicationController
# Pass a block for behavior when a user fails to save # Pass a block for behavior when a user fails to save
def register_automatically(user, &block) def register_automatically(user, &block)
# Automatic activation # Automatic activation
user.activate user.status = User::STATUS_ACTIVE
user.last_login_on = Time.now user.last_login_on = Time.now
if user.save if user.save
self.logged_user = user self.logged_user = user
-75
View File
@@ -1,75 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ActivitiesController < ApplicationController
menu_item :activity
before_filter :find_optional_project
accept_rss_auth :index
def index
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
private
# TODO: refactor, duplicated in projects_controller
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
end
+5 -3
View File
@@ -17,7 +17,9 @@
class AdminController < ApplicationController class AdminController < ApplicationController
layout 'admin' layout 'admin'
before_filter :require_admin before_filter :require_admin
helper :sort helper :sort
include SortHelper include SortHelper
@@ -28,10 +30,12 @@ class AdminController < ApplicationController
def projects def projects
@status = params[:status] ? params[:status].to_i : 1 @status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
unless params[:name].blank? unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%" name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
end end
@projects = Project.find :all, :order => 'lft', @projects = Project.find :all, :order => 'lft',
:conditions => c.conditions :conditions => c.conditions
@@ -73,9 +77,7 @@ class AdminController < ApplicationController
def info def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name @db_adapter_name = ActiveRecord::Base.connection.adapter_name
@checklist = [ @checklist = [
[:text_default_administrator_account_changed, [:text_default_administrator_account_changed, User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?],
User.find(:first,
:conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?],
[:text_file_repository_writable, File.writable?(Attachment.storage_path)], [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
[:text_plugin_assets_writable, File.writable?(Engines.public_directory)], [:text_plugin_assets_writable, File.writable?(Engines.public_directory)],
[:text_rmagick_available, Object.const_defined?(:Magick)] [:text_rmagick_available, Object.const_defined?(:Magick)]
+39 -263
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -18,13 +18,10 @@
require 'uri' require 'uri'
require 'cgi' require 'cgi'
class Unauthorized < Exception; end
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Redmine::I18n include Redmine::I18n
layout 'base' layout 'base'
exempt_from_layout 'builder', 'rsb'
# Remove broken cookie after upgrade from 0.8.x (#4292) # Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
@@ -43,13 +40,12 @@ class ApplicationController < ActionController::Base
protect_from_forgery protect_from_forgery
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
rescue_from ::Unauthorized, :with => :deny_access
include Redmine::Search::Controller include Redmine::Search::Controller
include Redmine::MenuManager::MenuController include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper helper Redmine::MenuManager::MenuHelper
Redmine::Scm::Base.all.each do |scm| REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}" require_dependency "repository/#{scm.underscore}"
end end
@@ -71,13 +67,13 @@ class ApplicationController < ActionController::Base
user = User.try_to_autologin(cookies[:autologin]) user = User.try_to_autologin(cookies[:autologin])
session[:user_id] = user.id if user session[:user_id] = user.id if user
user user
elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session # RSS key authentication does not start a session
User.find_by_rss_key(params[:key]) User.find_by_rss_key(params[:key])
elsif Setting.rest_api_enabled? && accept_api_auth? elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format]) && accept_key_auth_actions.include?(params[:action])
if (key = api_key_from_request) if params[:key].present?
# Use API key # Use API key
User.find_by_api_key(key) User.find_by_api_key(params[:key])
else else
# HTTP Basic, either username/password or API key/random # HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password| authenticate_with_http_basic do |username, password|
@@ -111,9 +107,8 @@ class ApplicationController < ActionController::Base
lang = find_language(User.current.language) lang = find_language(User.current.language)
end end
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE'] if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? if !accept_lang.blank?
accept_lang = accept_lang.downcase
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first) lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
end end
end end
@@ -132,9 +127,8 @@ class ApplicationController < ActionController::Base
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => "account", :action => "login", :back_url => url } format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url } format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } format.xml { head :unauthorized }
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } format.json { head :unauthorized }
format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
end end
return false return false
end end
@@ -156,16 +150,8 @@ class ApplicationController < ActionController::Base
# Authorize the user for the requested action # Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false) def authorize(ctrl = params[:controller], action = params[:action], global = false)
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global) allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
if allowed allowed ? true : deny_access
true
else
if @project && @project.archived?
render_403 :message => :notice_not_authorized_archived_project
else
deny_access
end
end
end end
# Authorize the user for the requested action outside a project # Authorize the user for the requested action outside a project
@@ -173,76 +159,6 @@ class ApplicationController < ActionController::Base
authorize(ctrl, action, global) authorize(ctrl, action, global)
end end
# Find project of id params[:id]
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find project of id params[:project_id]
def find_project_by_project_id
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find a project based on params[:project_id]
# TODO: some subclasses override this, see about merging their logic
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
rescue ActiveRecord::RecordNotFound
render_404
end
# Finds and sets @project based on @object.project
def find_project_from_association
render_404 unless @object.present?
@project = @object.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_model_object
model = self.class.read_inheritable_attribute('model_object')
if model
@object = model.find(params[:id])
self.instance_variable_set('@' + controller_name.singularize, @object) if @object
end
rescue ActiveRecord::RecordNotFound
render_404
end
def self.model_object(model)
write_inheritable_attribute('model_object', model)
end
# Filter for bulk issue operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
if @issues.detect {|issue| !issue.visible?}
deny_access
return
end
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
# Check if project is unique before bulk operations
def check_project_uniqueness
unless @project
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
end
end
# make sure that the user is a member of the project (or admin) if project is private # make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project # used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy def check_project_privacy
@@ -259,10 +175,6 @@ class ApplicationController < ActionController::Base
end end
end end
def back_url
params[:back_url] || request.env['HTTP_REFERER']
end
def redirect_back_or_default(default) def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s) back_url = CGI.unescape(params[:back_url].to_s)
if !back_url.blank? if !back_url.blank?
@@ -278,50 +190,25 @@ class ApplicationController < ActionController::Base
end end
end end
redirect_to default redirect_to default
false
end end
def render_403(options={}) def render_403
@project = nil @project = nil
render_error({:message => :notice_not_authorized, :status => 403}.merge(options)) render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403
return false return false
end end
def render_404(options={}) def render_404
render_error({:message => :notice_file_not_found, :status => 404}.merge(options)) render :template => "common/404", :layout => !request.xhr?, :status => 404
return false return false
end end
# Renders an error response def render_error(msg)
def render_error(arg) flash.now[:error] = msg
arg = {:message => arg} unless arg.is_a?(Hash) render :text => '', :layout => !request.xhr?, :status => 500
@message = arg[:message]
@message = l(@message) if @message.is_a?(Symbol)
@status = arg[:status] || 500
respond_to do |format|
format.html {
render :template => 'common/error', :layout => use_layout, :status => @status
}
format.atom { head @status }
format.xml { head @status }
format.js { head @status }
format.json { head @status }
end
end
# Picks which layout to use based on the request
#
# @return [boolean, string] name of the layout to use or false for no layout
def use_layout
request.xhr? ? false : 'base'
end end
def invalid_authenticity_token def invalid_authenticity_token
if api_request?
logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
end
render_error "Invalid form authenticity token." render_error "Invalid form authenticity token."
end end
@@ -333,40 +220,34 @@ class ApplicationController < ActionController::Base
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml' render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end end
# TODO: remove in Redmine 1.4
def self.accept_key_auth(*actions) def self.accept_key_auth(*actions)
ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." actions = actions.flatten.map(&:to_s)
accept_rss_auth(*actions) write_inheritable_attribute('accept_key_auth_actions', actions)
end end
# TODO: remove in Redmine 1.4
def accept_key_auth_actions def accept_key_auth_actions
ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." self.class.read_inheritable_attribute('accept_key_auth_actions') || []
self.class.accept_rss_auth
end end
def self.accept_rss_auth(*actions) # TODO: move to model
if actions.any? def attach_files(obj, attachments)
write_inheritable_attribute('accept_rss_auth_actions', actions) attached = []
else unsaved = []
read_inheritable_attribute('accept_rss_auth_actions') || [] if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
a.new_record? ? (unsaved << a) : (attached << a)
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
end end
end end
attached
def accept_rss_auth?(action=action_name)
self.class.accept_rss_auth.include?(action.to_sym)
end
def self.accept_api_auth(*actions)
if actions.any?
write_inheritable_attribute('accept_api_auth_actions', actions)
else
read_inheritable_attribute('accept_api_auth_actions') || []
end
end
def accept_api_auth?(action=action_name)
self.class.accept_api_auth.include?(action.to_sym)
end end
# Returns the number of objects that should be displayed # Returns the number of objects that should be displayed
@@ -384,30 +265,6 @@ class ApplicationController < ActionController::Base
per_page per_page
end end
# Returns offset and limit used to retrieve objects
# for an API response based on offset, limit and page parameters
def api_offset_and_limit(options=params)
if options[:offset].present?
offset = options[:offset].to_i
if offset < 0
offset = 0
end
end
limit = options[:limit].to_i
if limit < 1
limit = 25
elsif limit > 100
limit = 100
end
if offset.nil? && options[:page].present?
offset = (options[:page].to_i - 1) * limit
offset = 0 if offset < 0
end
offset ||= 0
[offset, limit]
end
# qvalues http header parser # qvalues http header parser
# code taken from webrick # code taken from webrick
def parse_qvalues(value) def parse_qvalues(value)
@@ -433,85 +290,4 @@ class ApplicationController < ActionController::Base
def filename_for_content_disposition(name) def filename_for_content_disposition(name)
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
end end
def api_request?
%w(xml json).include? params[:format]
end
# Returns the API key present in the request
def api_key_from_request
if params[:key].present?
params[:key]
elsif request.headers["X-Redmine-API-Key"].present?
request.headers["X-Redmine-API-Key"]
end
end
# Renders a warning flash if obj has unsaved attachments
def render_attachment_warning_if_needed(obj)
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
end
# Sets the `flash` notice or error based the number of issues that did not save
#
# @param [Array, Issue] issues all of the saved and unsaved Issues
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues,
:count => unsaved_issue_ids.size,
:total => issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
end
# Rescues an invalid query statement. Just in case...
def query_statement_invalid(exception)
logger.error "Query::StatementInvalid: #{exception.message}" if logger
session.delete(:query)
sort_clear if respond_to?(:sort_clear)
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
end
# Converts the errors on an ActiveRecord object into a common JSON format
def object_errors_to_json(object)
object.errors.collect do |attribute, error|
{ attribute => error }
end.to_json
end
# Renders API response on validation failure
def render_validation_errors(object)
options = { :status => :unprocessable_entity, :layout => false }
options.merge!(case params[:format]
when 'xml'; { :xml => object.errors }
when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
else
raise "Unknown format #{params[:format]} in #render_validation_errors"
end
)
render options
end
# Overrides #default_template so that the api template
# is used automatically if it exists
def default_template(action_name = self.action_name)
if api_request?
begin
return self.view_paths.find_template(default_template_name(action_name), 'api')
rescue ::ActionView::MissingTemplate
# the api template was not found
# fallback to the default behaviour
end
end
super
end
# Overrides #pick_layout so that #render with no arguments
# doesn't use the layout for api requests
def pick_layout(*args)
api_request? ? nil : super
end
end end
+1 -1
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+18 -21
View File
@@ -20,42 +20,45 @@ class AuthSourcesController < ApplicationController
before_filter :require_admin before_filter :require_admin
def index
list
render :action => 'list' unless request.xhr?
end
# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ], verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :template => :index } :redirect_to => { :action => :list }
def index def list
@auth_source_pages, @auth_sources = paginate auth_source_class.name.tableize, :per_page => 10 @auth_source_pages, @auth_sources = paginate :auth_sources, :per_page => 10
render "auth_sources/index" render :action => "list", :layout => false if request.xhr?
end end
def new def new
@auth_source = auth_source_class.new @auth_source = AuthSourceLdap.new
render 'auth_sources/new'
end end
def create def create
@auth_source = auth_source_class.new(params[:auth_source]) @auth_source = AuthSourceLdap.new(params[:auth_source])
if @auth_source.save if @auth_source.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index' redirect_to :action => 'list'
else else
render 'auth_sources/new' render :action => 'new'
end end
end end
def edit def edit
@auth_source = AuthSource.find(params[:id]) @auth_source = AuthSource.find(params[:id])
render 'auth_sources/edit'
end end
def update def update
@auth_source = AuthSource.find(params[:id]) @auth_source = AuthSource.find(params[:id])
if @auth_source.update_attributes(params[:auth_source]) if @auth_source.update_attributes(params[:auth_source])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'list'
else else
render 'auth_sources/edit' render :action => 'edit'
end end
end end
@@ -65,9 +68,9 @@ class AuthSourcesController < ApplicationController
@auth_method.test_connection @auth_method.test_connection
flash[:notice] = l(:notice_successful_connection) flash[:notice] = l(:notice_successful_connection)
rescue => text rescue => text
flash[:error] = l(:error_unable_to_connect, text.message) flash[:error] = "Unable to connect (#{text})"
end end
redirect_to :action => 'index' redirect_to :action => 'list'
end end
def destroy def destroy
@@ -76,12 +79,6 @@ class AuthSourcesController < ApplicationController
@auth_source.destroy @auth_source.destroy
flash[:notice] = l(:notice_successful_delete) flash[:notice] = l(:notice_successful_delete)
end end
redirect_to :action => 'index' redirect_to :action => 'list'
end
protected
def auth_source_class
AuthSource
end end
end end
@@ -1,27 +0,0 @@
class AutoCompletesController < ApplicationController
before_filter :find_project
def issues
@issues = []
q = params[:q].to_s
query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues
if q.match(/^\d+$/)
@issues << query.visible.find_by_id(q.to_i)
end
unless q.blank?
@issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
end
@issues.compact!
render :layout => false
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end
+6 -16
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,8 +17,7 @@
class BoardsController < ApplicationController class BoardsController < ApplicationController
default_search_scope :messages default_search_scope :messages
before_filter :find_project, :find_board_if_available, :authorize before_filter :find_project, :authorize
accept_rss_auth :index, :show
helper :messages helper :messages
include MessagesHelper include MessagesHelper
@@ -69,33 +68,24 @@ class BoardsController < ApplicationController
@board.project = @project @board.project = @project
if request.post? && @board.save if request.post? && @board.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to_settings_in_projects redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end end
end end
def edit def edit
if request.post? && @board.update_attributes(params[:board]) if request.post? && @board.update_attributes(params[:board])
redirect_to_settings_in_projects redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end end
end end
def destroy def destroy
@board.destroy @board.destroy
redirect_to_settings_in_projects
end
private
def redirect_to_settings_in_projects
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end end
private
def find_project def find_project
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_board_if_available
@board = @project.boards.find(params[:id]) if params[:id] @board = @project.boards.find(params[:id]) if params[:id]
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
-56
View File
@@ -1,56 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CalendarsController < ApplicationController
menu_item :calendar
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :issues
helper :projects
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def show
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
@query.group_by = nil
if @query.valid?
events = []
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
render :action => 'show', :layout => false if request.xhr?
end
end
-36
View File
@@ -1,36 +0,0 @@
class CommentsController < ApplicationController
default_search_scope :news
model_object News
before_filter :find_model_object
before_filter :find_project_from_association
before_filter :authorize
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
end
redirect_to :controller => 'news', :action => 'show', :id => @news
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
@news.comments.find(params[:comment_id]).destroy
redirect_to :controller => 'news', :action => 'show', :id => @news
end
private
# ApplicationController's find_model_object sets it based on the controller
# name so it needs to be overriden and set to @news instead
def find_model_object
super
@news = @object
@comment = nil
@news
end
end
@@ -1,57 +0,0 @@
class ContextMenusController < ApplicationController
helper :watchers
helper :issues
def issues
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
else
@allowed_statuses = @issues.map do |i|
i.new_statuses_allowed_to(User.current)
end.inject do |memo,s|
memo & s
end
end
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => User.current.allowed_to?(:delete_issues, @projects)
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
else
#when multiple projects, we only keep the intersection of each set
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
end
@priorities = IssuePriority.all.reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = back_url
render :layout => false
end
def time_entries
@time_entries = TimeEntry.all(
:conditions => {:id => params[:ids]}, :include => :project)
@projects = @time_entries.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
@activities = TimeEntryActivity.shared.active
@can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
:delete => User.current.allowed_to?(:edit_time_entries, @projects)
}
@back = back_url
render :layout => false
end
end
+3 -5
View File
@@ -38,9 +38,8 @@ class CustomFieldsController < ApplicationController
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field) call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
else
@trackers = Tracker.find(:all, :order => 'position')
end end
@trackers = Tracker.find(:all, :order => 'position')
end end
def edit def edit
@@ -49,16 +48,15 @@ class CustomFieldsController < ApplicationController
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field) call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
else
@trackers = Tracker.find(:all, :order => 'position')
end end
@trackers = Tracker.find(:all, :order => 'position')
end end
def destroy def destroy
@custom_field = CustomField.find(params[:id]).destroy @custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
rescue rescue
flash[:error] = l(:error_can_not_delete_custom_field) flash[:error] = "Unable to delete custom field"
redirect_to :action => 'index' redirect_to :action => 'index'
end end
end end
+11 -9
View File
@@ -17,10 +17,8 @@
class DocumentsController < ApplicationController class DocumentsController < ApplicationController
default_search_scope :documents default_search_scope :documents
model_object Document
before_filter :find_project, :only => [:index, :new] before_filter :find_project, :only => [:index, :new]
before_filter :find_model_object, :except => [:index, :new] before_filter :find_document, :except => [:index, :new]
before_filter :find_project_from_association, :except => [:index, :new]
before_filter :authorize before_filter :authorize
helper :attachments helper :attachments
@@ -49,8 +47,7 @@ class DocumentsController < ApplicationController
def new def new
@document = @project.documents.build(params[:document]) @document = @project.documents.build(params[:document])
if request.post? and @document.save if request.post? and @document.save
attachments = Attachment.attach_files(@document, params[:attachments]) attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'index', :project_id => @project
end end
@@ -70,10 +67,8 @@ class DocumentsController < ApplicationController
end end
def add_attachment def add_attachment
attachments = Attachment.attach_files(@document, params[:attachments]) attachments = attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document) Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document redirect_to :action => 'show', :id => @document
end end
@@ -83,4 +78,11 @@ private
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def find_document
@document = Document.find(params[:id])
@project = @document.project
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+2 -4
View File
@@ -75,15 +75,13 @@ class EnumerationsController < ApplicationController
# No associated objects # No associated objects
@enumeration.destroy @enumeration.destroy
redirect_to :action => 'index' redirect_to :action => 'index'
return
elsif params[:reassign_to_id] elsif params[:reassign_to_id]
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id]) if reassign_to = Enumeration.find_by_type_and_id(@enumeration.type, params[:reassign_to_id])
@enumeration.destroy(reassign_to) @enumeration.destroy(reassign_to)
redirect_to :action => 'index' redirect_to :action => 'index'
return
end end
end end
@enumerations = @enumeration.class.find(:all) - [@enumeration] @enumerations = Enumeration.find(:all, :conditions => ['type = (?)', @enumeration.type]) - [@enumeration]
#rescue #rescue
# flash[:error] = 'Unable to delete enumeration' # flash[:error] = 'Unable to delete enumeration'
# redirect_to :action => 'index' # redirect_to :action => 'index'
-36
View File
@@ -1,36 +0,0 @@
class FilesController < ApplicationController
menu_item :files
before_filter :find_project_by_project_id
before_filter :authorize
helper :sort
include SortHelper
def index
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
def new
@versions = @project.versions.sort
end
def create
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = Attachment.attach_files(container, params[:attachments])
render_attachment_warning_if_needed(container)
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments[:files])
end
redirect_to project_files_path(@project)
end
end
-48
View File
@@ -1,48 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class GanttsController < ApplicationController
menu_item :gantt
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :gantt
helper :issues
helper :projects
helper :queries
include QueriesHelper
helper :sort
include SortHelper
include Redmine::Export::PDF
def show
@gantt = Redmine::Helpers::Gantt.new(params)
@gantt.project = @project
retrieve_query
@query.group_by = nil
@gantt.query = @query if @query.valid?
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
end
end
end
+4 -11
View File
@@ -57,7 +57,7 @@ class GroupsController < ApplicationController
# GET /groups/1/edit # GET /groups/1/edit
def edit def edit
@group = Group.find(params[:id], :include => :projects) @group = Group.find(params[:id])
end end
# POST /groups # POST /groups
@@ -132,16 +132,16 @@ class GroupsController < ApplicationController
def autocomplete_for_user def autocomplete_for_user
@group = Group.find(params[:id]) @group = Group.find(params[:id])
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100) @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
render :layout => false render :layout => false
end end
def edit_membership def edit_membership
@group = Group.find(params[:id]) @group = Group.find(params[:id])
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group) @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
@membership.attributes = params[:membership]
@membership.save if request.post? @membership.save if request.post?
respond_to do |format| respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js { format.js {
render(:update) {|page| render(:update) {|page|
@@ -149,13 +149,6 @@ class GroupsController < ApplicationController
page.visual_effect(:highlight, "member-#{@membership.id}") page.visual_effect(:highlight, "member-#{@membership.id}")
} }
} }
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end end
end end
+3 -42
View File
@@ -17,41 +17,10 @@
class IssueCategoriesController < ApplicationController class IssueCategoriesController < ApplicationController
menu_item :settings menu_item :settings
model_object IssueCategory before_filter :find_project, :authorize
before_filter :find_model_object, :except => :new
before_filter :find_project_from_association, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
verify :method => :post, :only => :destroy verify :method => :post, :only => :destroy
def new
@category = @project.issue_categories.build(params[:category])
if request.post?
if @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_category_id",
content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
}
end
end
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
end
end
end
end
end
def edit def edit
if request.post? and @category.update_attributes(params[:category]) if request.post? and @category.update_attributes(params[:category])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
@@ -65,26 +34,18 @@ class IssueCategoriesController < ApplicationController
# No issue assigned to this category # No issue assigned to this category
@category.destroy @category.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
return
elsif params[:todo] elsif params[:todo]
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign' reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
@category.destroy(reassign_to) @category.destroy(reassign_to)
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
return
end end
@categories = @project.issue_categories - [@category] @categories = @project.issue_categories - [@category]
end end
private private
# Wrap ApplicationController's find_model_object method to set
# @category instead of just @issue_category
def find_model_object
super
@category = @object
end
def find_project def find_project
@project = Project.find(params[:project_id]) @category = IssueCategory.find(params[:id])
@project = @category.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
-68
View File
@@ -1,68 +0,0 @@
class IssueMovesController < ApplicationController
default_search_scope :issues
before_filter :find_issues, :check_project_uniqueness
before_filter :authorize
def new
prepare_for_issue_move
render :layout => false if request.xhr?
end
def create
prepare_for_issue_move
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
issue.reload
issue.init_journal(User.current)
issue.current_journal.notes = @notes if @notes.present?
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end
return
end
end
private
def prepare_for_issue_move
@issues.sort!
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = Issue.allowed_target_projects_on_move
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
@notes = params[:notes]
@notes ||= ''
end
def extract_changed_attributes_for_move(params)
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
changed_attributes
end
end
+8 -11
View File
@@ -1,4 +1,4 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@@ -16,19 +16,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueRelationsController < ApplicationController class IssueRelationsController < ApplicationController
before_filter :find_issue, :find_project_from_association, :authorize before_filter :find_project, :authorize
def new def new
@relation = IssueRelation.new(params[:relation]) @relation = IssueRelation.new(params[:relation])
@relation.issue_from = @issue @relation.issue_from = @issue
if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/) if params[:relation] && !params[:relation][:issue_to_id].blank?
@relation.issue_to = Issue.visible.find_by_id(m[1].to_i) @relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id])
end end
@relation.save if request.post? @relation.save if request.post?
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js do format.js do
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render :update do |page| render :update do |page|
page.replace_html "relations", :partial => 'issues/relations' page.replace_html "relations", :partial => 'issues/relations'
if @relation.errors.empty? if @relation.errors.empty?
@@ -48,16 +47,14 @@ class IssueRelationsController < ApplicationController
end end
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js { format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'}
}
end end
end end
private private
def find_issue def find_project
@issue = @object = Issue.find(params[:issue_id]) @issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+13 -8
View File
@@ -21,11 +21,16 @@ class IssueStatusesController < ApplicationController
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ], verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ],
:redirect_to => { :action => :index } :redirect_to => { :action => :list }
def index def index
list
render :action => 'list' unless request.xhr?
end
def list
@issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position" @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position"
render :action => "index", :layout => false if request.xhr? render :action => "list", :layout => false if request.xhr?
end end
def new def new
@@ -36,7 +41,7 @@ class IssueStatusesController < ApplicationController
@issue_status = IssueStatus.new(params[:issue_status]) @issue_status = IssueStatus.new(params[:issue_status])
if @issue_status.save if @issue_status.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index' redirect_to :action => 'list'
else else
render :action => 'new' render :action => 'new'
end end
@@ -50,7 +55,7 @@ class IssueStatusesController < ApplicationController
@issue_status = IssueStatus.find(params[:id]) @issue_status = IssueStatus.find(params[:id])
if @issue_status.update_attributes(params[:issue_status]) if @issue_status.update_attributes(params[:issue_status])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'list'
else else
render :action => 'edit' render :action => 'edit'
end end
@@ -58,10 +63,10 @@ class IssueStatusesController < ApplicationController
def destroy def destroy
IssueStatus.find(params[:id]).destroy IssueStatus.find(params[:id]).destroy
redirect_to :action => 'index' redirect_to :action => 'list'
rescue rescue
flash[:error] = l(:error_unable_delete_issue_status) flash[:error] = "Unable to delete issue status"
redirect_to :action => 'index' redirect_to :action => 'list'
end end
def update_issue_done_ratio def update_issue_done_ratio
@@ -70,6 +75,6 @@ class IssueStatusesController < ApplicationController
else else
flash[:error] = l(:error_issue_done_ratios_not_updated) flash[:error] = l(:error_issue_done_ratios_not_updated)
end end
redirect_to :action => 'index' redirect_to :action => 'list'
end end
end end
+370 -164
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,19 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController class IssuesController < ApplicationController
menu_item :new_issue, :only => [:new, :create] menu_item :new_issue, :only => :new
default_search_scope :issues default_search_scope :issues
before_filter :find_issue, :only => [:show, :edit, :update] before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy] before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :check_project_uniqueness, :only => [:move, :perform_move] before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :find_project, :only => [:new, :create] before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
before_filter :authorize, :except => [:index] before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
before_filter :find_optional_project, :only => [:index] accept_key_auth :index, :show, :changes
before_filter :check_for_default_issue_status, :only => [:new, :create]
before_filter :build_new_issue_from_params, :only => [:new, :create]
accept_rss_auth :index, :show
accept_api_auth :index, :show, :create, :update, :destroy
rescue_from Query::StatementInvalid, :with => :query_statement_invalid rescue_from Query::StatementInvalid, :with => :query_statement_invalid
@@ -45,52 +41,40 @@ class IssuesController < ApplicationController
include AttachmentsHelper include AttachmentsHelper
helper :queries helper :queries
include QueriesHelper include QueriesHelper
helper :repositories
include RepositoriesHelper
helper :sort helper :sort
include SortHelper include SortHelper
include IssuesHelper include IssuesHelper
helper :timelog helper :timelog
helper :gantt
include Redmine::Export::PDF include Redmine::Export::PDF
verify :method => [:post, :delete], verify :method => :post,
:only => :destroy, :only => :destroy,
:render => { :nothing => true, :status => :method_not_allowed } :render => { :nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def index def index
retrieve_query retrieve_query
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria) sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns) sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid? if @query.valid?
case params[:format] limit = per_page_option
when 'csv', 'pdf' respond_to do |format|
@limit = Setting.issues_export_limit.to_i format.html { }
when 'atom' format.atom { limit = Setting.feeds_limit.to_i }
@limit = Setting.feeds_limit.to_i format.csv { limit = Setting.issues_export_limit.to_i }
when 'xml', 'json' format.pdf { limit = Setting.issues_export_limit.to_i }
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end end
@issue_count = @query.issue_count @issue_count = @query.issue_count
@issue_pages = Paginator.new self, @issue_count, @limit, params['page'] @issue_pages = Paginator.new self, @issue_count, limit, params['page']
@offset ||= @issue_pages.current.offset
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version], @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => sort_clause, :order => sort_clause,
:offset => @offset, :offset => @issue_pages.current.offset,
:limit => @limit) :limit => limit)
@issue_count_by_group = @query.issue_count_by_group @issue_count_by_group = @query.issue_count_by_group
respond_to do |format| respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.api
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') } format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') } format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
@@ -103,25 +87,34 @@ class IssuesController < ApplicationController
render_404 render_404
end end
def changes
retrieve_query
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def show def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1} @journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order? @journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets
if User.current.allowed_to?(:view_changesets, @project)
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order? @changesets.reverse! if User.current.wants_comments_in_reverse_order?
end
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all @priorities = IssuePriority.all
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) @time_entry = TimeEntry.new
respond_to do |format| respond_to do |format|
format.html { render :template => 'issues/show.rhtml' } format.html { render :template => 'issues/show.rhtml' }
format.api format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end end
end end
@@ -129,95 +122,212 @@ class IssuesController < ApplicationController
# Add a new issue # Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given # The new issue will be created from an existing one if copy_from parameter is given
def new def new
respond_to do |format| @issue = Issue.new
format.html { render :action => 'new', :layout => !request.xhr? } @issue.copy_from(params[:copy_from]) if params[:copy_from]
format.js { render :partial => 'attributes' } @issue.project = @project
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
render_error l(:error_no_tracker_in_project)
return
end end
if params[:issue].is_a?(Hash)
@issue.attributes = params[:issue]
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end end
@issue.author = User.current
def create default_status = IssueStatus.default
unless default_status
render_error l(:error_no_default_issue_status)
return
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
if request.get? || request.xhr?
@issue.start_date ||= Date.today
else
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
if @issue.save if @issue.save
attachments = Attachment.attach_files(@issue, params[:attachments]) attach_files(@issue, params[:attachments])
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
respond_to do |format| redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
format.html {
redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
{ :action => 'show', :id => @issue }) { :action => 'show', :id => @issue })
}
format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
end
return return
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@issue) }
end end
end end
@priorities = IssuePriority.all
render :layout => !request.xhr?
end end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit def edit
update_issue_from_params @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@journal = @issue.current_journal @notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
respond_to do |format| # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
format.html { } if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
format.xml { } attrs = params[:issue].dup
end attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end end
def update if request.post?
update_issue_from_params @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
if @issue.save_issue_with_child_records(params, @time_entry) if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
render_attachment_warning_if_needed(@issue) attachments = attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
respond_to do |format| if @issue.save
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) } # Log spend time
format.api { head :ok } if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_back_or_default({:action => 'show', :id => @issue})
end
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
# Remove the previously added attachments if issue was not updated
attachments.each(&:destroy)
end
def reply
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else else
render_attachment_warning_if_needed(@issue) user = @issue.author
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? text = @issue.description
@journal = @issue.current_journal
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@issue) }
end
end end
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end end
# Bulk edit a set of issues # Bulk edit a set of issues
def bulk_edit def bulk_edit
@issues.sort! if request.post?
@available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w} tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
@custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c} status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a} priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t} assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
end category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
def bulk_update custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
@issues.sort!
attributes = parse_params_for_bulk_issue_attributes(params)
unsaved_issue_ids = [] unsaved_issue_ids = []
@issues.each do |issue| @issues.each do |issue|
issue.reload
journal = issue.init_journal(User.current, params[:notes]) journal = issue.init_journal(User.current, params[:notes])
issue.safe_attributes = attributes issue.tracker = tracker if tracker
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none'
issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
unless issue.save # Don't save any change to the issue if the user is not authorized to apply the requested status
unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
# Keep unsaved issue ids to display them in flash error # Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id unsaved_issue_ids << issue.id
end end
end end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
:total => @issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
return
end
@available_statuses = Workflow.available_statuses(@project)
@custom_fields = @project.all_issue_custom_fields
end
def move
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
issue.init_journal(User.current)
if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
:total => @issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end
return
end
render :layout => false if request.xhr?
end end
def destroy def destroy
@@ -237,100 +347,196 @@ class IssuesController < ApplicationController
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues]) TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
end end
else else
# display the destroy form if it's a user request # display the destroy form
return unless api_request? return
end end
end end
@issues.each do |issue| @issues.each(&:destroy)
begin redirect_to :action => 'index', :project_id => @project
issue.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
# nothing to do, issue was already deleted (eg. by a parent)
end end
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
retrieve_query
if @query.valid?
events = []
# Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:order => "start_date, due_date",
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
:order => "start_date, effective_date",
:conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format| respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) } format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.api { head :ok } format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
end end
end end
def calendar
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
if @query.valid?
events = []
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
render :layout => false if request.xhr?
end
def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
end
@priorities = IssuePriority.all.reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = params[:back_url] || request.env['HTTP_REFERER']
render :layout => false
end
def update_form
if params[:id].blank?
@issue = Issue.new
@issue.project = @project
else
@issue = @project.issues.visible.find(params[:id])
end
@issue.attributes = params[:issue]
@allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
@priorities = IssuePriority.all
render :partial => 'attributes'
end
def preview
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
@attachements = @issue.attachments if @issue
@text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
render :partial => 'common/preview'
end
private private
def find_issue def find_issue
# Issue.visible.find(...) can not be used to redirect user to the login form
# if the issue actually exists but requires authentication
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
unless @issue.visible?
deny_access
return
end
@project = @issue.project @project = @issue.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def find_project # Filter for bulk operations
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] def find_issues
@project = Project.find(project_id) @issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
# Used by #edit and #update to set some common instance variables def find_project
# from the params @project = Project.find(params[:project_id])
# TODO: Refactor, not everything in here is needed by #edit rescue ActiveRecord::RecordNotFound
def update_issue_from_params render_404
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
@time_entry.attributes = params[:time_entry]
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
@issue.init_journal(User.current, @notes)
@issue.safe_attributes = params[:issue]
end end
# TODO: Refactor, lots of extra code in here def find_optional_project
# TODO: Changing tracker on an existing issue should not trigger this @project = Project.find(params[:project_id]) unless params[:project_id].blank?
def build_new_issue_from_params allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
if params[:id].blank? allowed ? true : deny_access
@issue = Issue.new rescue ActiveRecord::RecordNotFound
@issue.copy_from(params[:copy_from]) if params[:copy_from] render_404
@issue.project = @project end
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else else
@issue = @project.issues.visible.find(params[:id]) if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end end
else
@issue.project = @project @query.available_filters.keys.each do |field|
@issue.author = User.current @query.add_short_filter(field, params[field]) if params[field]
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
render_error l(:error_no_tracker_in_project)
return false
end
@issue.start_date ||= Date.today
if params[:issue].is_a?(Hash)
@issue.safe_attributes = params[:issue]
if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
@issue.watcher_user_ids = params[:issue]['watcher_user_ids']
end end
end end
@priorities = IssuePriority.all @query.group_by = params[:group_by]
@allowed_statuses = @issue.new_statuses_allowed_to(User.current, true) @query.column_names = params[:query] && params[:query][:column_names]
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
@query.project = @project
end end
def check_for_default_issue_status
if IssueStatus.default.nil?
render_error l(:error_no_default_issue_status)
return false
end end
end end
def parse_params_for_bulk_issue_attributes(params) # Rescues an invalid query statement. Just in case...
attributes = (params[:issue] || {}).reject {|k,v| v.blank?} def query_statement_invalid(exception)
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} logger.error "Query::StatementInvalid: #{exception.message}" if logger
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values] session.delete(:query)
attributes sort_clear
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
end end
end end
+4 -82
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,71 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit, :diff] before_filter :find_journal
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit, :diff]
accept_rss_auth :index
menu_item :issues
helper :issues
helper :custom_fields
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def index
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def diff
@issue = @journal.issue
if params[:detail_id].present?
@detail = @journal.details.find_by_id(params[:detail_id])
else
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
end
(render_404; return false) unless @issue && @detail
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
def edit def edit
(render_403; return false) unless @journal.editable_by?(User.current)
if request.post? if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes] @journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank? @journal.destroy if @journal.details.empty? && @journal.notes.blank?
@@ -89,31 +27,15 @@ class JournalsController < ApplicationController
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id } format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' } format.js { render :action => 'update' }
end end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end end
end end
private private
def find_journal def find_journal
@journal = Journal.find(params[:id]) @journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
@project = @journal.journalized.project @project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+2 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+15 -26
View File
@@ -16,9 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController class MembersController < ApplicationController
model_object Member before_filter :find_member, :except => [:new, :autocomplete_for_member]
before_filter :find_model_object, :except => [:new, :autocomplete_for_member]
before_filter :find_project_from_association, :except => [:new, :autocomplete_for_member]
before_filter :find_project, :only => [:new, :autocomplete_for_member] before_filter :find_project, :only => [:new, :autocomplete_for_member]
before_filter :authorize before_filter :authorize
@@ -36,30 +34,13 @@ class MembersController < ApplicationController
@project.members << members @project.members << members
end end
respond_to do |format| respond_to do |format|
if members.present? && members.all? {|m| m.valid? }
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { format.js {
render(:update) {|page| render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members' page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page << 'hideOnLoad()'
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") } members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
} }
} }
else
format.js {
render(:update) {|page|
errors = members.collect {|m|
m.errors.full_messages
}.flatten.uniq
page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
}
}
end
end end
end end
@@ -70,7 +51,6 @@ class MembersController < ApplicationController
format.js { format.js {
render(:update) {|page| render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members' page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page << 'hideOnLoad()'
page.visual_effect(:highlight, "member-#{@member.id}") page.visual_effect(:highlight, "member-#{@member.id}")
} }
} }
@@ -84,11 +64,7 @@ class MembersController < ApplicationController
end end
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page << 'hideOnLoad()'
}
}
end end
end end
@@ -97,4 +73,17 @@ class MembersController < ApplicationController
render :layout => false render :layout => false
end end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_member
@member = Member.find(params[:id])
@project = @member.project
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+8 -25
View File
@@ -29,24 +29,10 @@ class MessagesController < ApplicationController
helper :attachments helper :attachments
include AttachmentsHelper include AttachmentsHelper
REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
# Show a topic and its replies # Show a topic and its replies
def show def show
page = params[:page] @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
# Find the page of the requested reply @replies.reverse! if User.current.wants_comments_in_reverse_order?
if params[:r] && page.nil?
offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
page = 1 + offset / REPLIES_PER_PAGE
end
@reply_count = @topic.children.count
@reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
:order => "#{Message.table_name}.created_on ASC",
:limit => @reply_pages.items_per_page,
:offset => @reply_pages.current.offset)
@reply = Message.new(:subject => "RE: #{@message.subject}") @reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr? render :action => "show", :layout => false if request.xhr?
end end
@@ -62,8 +48,7 @@ class MessagesController < ApplicationController
end end
if request.post? && @message.save if request.post? && @message.save
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
attachments = Attachment.attach_files(@message, params[:attachments]) attach_files(@message, params[:attachments])
render_attachment_warning_if_needed(@message)
redirect_to :action => 'show', :id => @message redirect_to :action => 'show', :id => @message
end end
end end
@@ -76,10 +61,9 @@ class MessagesController < ApplicationController
@topic.children << @reply @topic.children << @reply
if !@reply.new_record? if !@reply.new_record?
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
attachments = Attachment.attach_files(@reply, params[:attachments]) attach_files(@reply, params[:attachments])
render_attachment_warning_if_needed(@reply)
end end
redirect_to :action => 'show', :id => @topic, :r => @reply redirect_to :action => 'show', :id => @topic
end end
# Edit a message # Edit a message
@@ -90,11 +74,10 @@ class MessagesController < ApplicationController
@message.sticky = params[:message]['sticky'] @message.sticky = params[:message]['sticky']
end end
if request.post? && @message.update_attributes(params[:message]) if request.post? && @message.update_attributes(params[:message])
attachments = Attachment.attach_files(@message, params[:attachments]) attach_files(@message, params[:attachments])
render_attachment_warning_if_needed(@message)
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
@message.reload @message.reload
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id) redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
end end
end end
@@ -104,7 +87,7 @@ class MessagesController < ApplicationController
@message.destroy @message.destroy
redirect_to @message.parent.nil? ? redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent, :r => @message } { :action => 'show', :id => @message.parent }
end end
def quote def quote
+10 -4
View File
@@ -19,7 +19,6 @@ class MyController < ApplicationController
before_filter :require_login before_filter :require_login
helper :issues helper :issues
helper :users
helper :custom_fields helper :custom_fields
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
@@ -54,24 +53,31 @@ class MyController < ApplicationController
@user = User.current @user = User.current
@pref = @user.pref @pref = @user.pref
if request.post? if request.post?
@user.safe_attributes = params[:user] @user.attributes = params[:user]
@user.mail_notification = (params[:notification_option] == 'all')
@user.pref.attributes = params[:pref] @user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1') @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save if @user.save
@user.pref.save @user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
set_language_if_valid @user.language set_language_if_valid @user.language
flash[:notice] = l(:notice_account_updated) flash[:notice] = l(:notice_account_updated)
redirect_to :action => 'account' redirect_to :action => 'account'
return return
end end
end end
@notification_options = [[l(:label_user_mail_option_all), 'all'],
[l(:label_user_mail_option_none), 'none']]
# Only users that belong to more than 1 project can select projects for which they are notified
# Note that @user.membership.size would fail since AR ignores :include association option when doing a count
@notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
@notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
end end
# Manage user's password # Manage user's password
def password def password
@user = User.current @user = User.current
unless @user.change_password_allowed? if @user.auth_source_id
flash[:error] = l(:notice_can_t_change_password) flash[:error] = l(:notice_can_t_change_password)
redirect_to :action => 'account' redirect_to :action => 'account'
return return
+43 -41
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,38 +17,22 @@
class NewsController < ApplicationController class NewsController < ApplicationController
default_search_scope :news default_search_scope :news
model_object News before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_model_object, :except => [:new, :create, :index] before_filter :find_project, :only => [:new, :preview]
before_filter :find_project_from_association, :except => [:new, :create, :index] before_filter :authorize, :except => [:index, :preview]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index]
before_filter :find_optional_project, :only => :index before_filter :find_optional_project, :only => :index
accept_rss_auth :index accept_key_auth :index
accept_api_auth :index
helper :watchers
def index def index
case params[:format] @news_pages, @newss = paginate :news,
when 'xml', 'json' :per_page => 10,
@offset, @limit = api_offset_and_limit :conditions => Project.allowed_to_condition(User.current, :view_news, :project => @project),
else :include => [:author, :project],
@limit = 10 :order => "#{News.table_name}.created_on DESC"
end
scope = @project ? @project.news.visible : News.visible
@news_count = scope.count
@news_pages = Paginator.new self, @news_count, @limit, params['page']
@offset ||= @news_pages.current.offset
@newss = scope.all(:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC",
:offset => @offset,
:limit => @limit)
respond_to do |format| respond_to do |format|
format.html { render :layout => false if request.xhr? } format.html { render :layout => false if request.xhr? }
format.api format.xml { render :xml => @newss.to_xml }
format.json { render :json => @newss.to_json }
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end end
end end
@@ -60,39 +44,57 @@ class NewsController < ApplicationController
def new def new
@news = News.new(:project => @project, :author => User.current) @news = News.new(:project => @project, :author => User.current)
end
def create
@news = News.new(:project => @project, :author => User.current)
if request.post? if request.post?
@news.attributes = params[:news] @news.attributes = params[:news]
if @news.save if @news.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'news', :action => 'index', :project_id => @project redirect_to :controller => 'news', :action => 'index', :project_id => @project
else
render :action => 'new'
end end
end end
end end
def edit def edit
end if request.post? and @news.update_attributes(params[:news])
def update
if request.put? and @news.update_attributes(params[:news])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @news redirect_to :action => 'show', :id => @news
else
render :action => 'edit'
end end
end end
def add_comment
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
else
show
render :action => 'show'
end
end
def destroy_comment
@news.comments.find(params[:comment_id]).destroy
redirect_to :action => 'show', :id => @news
end
def destroy def destroy
@news.destroy @news.destroy
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'index', :project_id => @project
end end
def preview
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private private
def find_news
@news = News.find(params[:id])
@project = @news.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project def find_project
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
-50
View File
@@ -1,50 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class PreviewsController < ApplicationController
before_filter :find_project
def issue
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
if @issue
@attachements = @issue.attachments
@description = params[:issue] && params[:issue][:description]
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
@description = nil
end
@notes = params[:notes]
else
@description = (params[:issue] ? params[:issue][:description] : nil)
end
render :layout => false
end
def news
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -1,26 +0,0 @@
class ProjectEnumerationsController < ApplicationController
before_filter :find_project_by_project_id
before_filter :authorize
def update
if request.put? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def destroy
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
end
+224 -74
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,17 +17,19 @@
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
menu_item :overview menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings menu_item :settings, :only => :settings
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] before_filter :find_optional_project, :only => :activity
before_filter :authorize_global, :only => [:new, :create] before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
before_filter :authorize_global, :only => :add
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
accept_rss_auth :index accept_key_auth :activity
accept_api_auth :index, :show, :create, :update, :destroy
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
if controller.request.post? if controller.request.post?
controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt' controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
end end
@@ -38,6 +40,7 @@ class ProjectsController < ApplicationController
helper :custom_fields helper :custom_fields
include CustomFieldsHelper include CustomFieldsHelper
helper :issues helper :issues
helper IssuesHelper
helper :queries helper :queries
include QueriesHelper include QueriesHelper
helper :repositories helper :repositories
@@ -50,11 +53,6 @@ class ProjectsController < ApplicationController
format.html { format.html {
@projects = Project.visible.find(:all, :order => 'lft') @projects = Project.visible.find(:all, :order => 'lft')
} }
format.api {
@offset, @limit = api_offset_and_limit
@project_count = Project.visible.count
@projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
}
format.atom { format.atom {
projects = Project.visible.find(:all, :order => 'created_on DESC', projects = Project.visible.find(:all, :order => 'created_on DESC',
:limit => Setting.feeds_limit.to_i) :limit => Setting.feeds_limit.to_i)
@@ -63,19 +61,18 @@ class ProjectsController < ApplicationController
end end
end end
def new # Add a new project
def add
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all @trackers = Tracker.all
@project = Project.new(params[:project]) @project = Project.new(params[:project])
end if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } @project.trackers = Tracker.all
def create @project.is_public = Setting.default_projects_public?
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @project.enabled_module_names = Setting.default_projects_modules
@trackers = Tracker.all else
@project = Project.new @project.enabled_module_names = params[:enabled_modules]
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin # Add current user as a project member if he is not admin
@@ -84,20 +81,10 @@ class ProjectsController < ApplicationController
m = Member.new(:user => User.current, :roles => [r]) m = Member.new(:user => User.current, :roles => [r])
@project.members << m @project.members << m
end end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project redirect_to :controller => 'projects', :action => 'settings', :id => @project
}
format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@project) }
end end
end end
end end
def copy def copy
@@ -115,20 +102,18 @@ class ProjectsController < ApplicationController
redirect_to :controller => 'admin', :action => 'projects' redirect_to :controller => 'admin', :action => 'projects'
end end
else else
Mailer.with_deliveries(params[:notifications] == '1') do @project = Project.new(params[:project])
@project = Project.new @project.enabled_module_names = params[:enabled_modules]
@project.safe_attributes = params[:project]
if validate_parent_id && @project.copy(@source_project, :only => params[:only]) if validate_parent_id && @project.copy(@source_project, :only => params[:only])
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project redirect_to :controller => 'admin', :action => 'projects'
elsif !@project.new_record? elsif !@project.new_record?
# Project was created # Project was created
# But some objects were not copied due to validation failures # But some objects were not copied due to validation failures
# (eg. issues from disabled trackers) # (eg. issues from disabled trackers)
# TODO: inform about that # TODO: inform about that
redirect_to :controller => 'projects', :action => 'settings', :id => @project redirect_to :controller => 'admin', :action => 'projects'
end
end end
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
@@ -143,7 +128,7 @@ class ProjectsController < ApplicationController
end end
@users_by_role = @project.users_by_role @users_by_role = @project.users_by_role
@subprojects = @project.children.visible.all @subprojects = @project.children.visible
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers @trackers = @project.rolled_up_trackers
@@ -156,16 +141,12 @@ class ProjectsController < ApplicationController
:include => [:project, :status, :tracker], :include => [:project, :status, :tracker],
:conditions => cond) :conditions => cond)
if User.current.allowed_to?(:view_time_entries, @project) TimeEntry.visible_by(User.current) do
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f @total_hours = TimeEntry.sum(:hours,
:include => :project,
:conditions => cond).to_f
end end
@key = User.current.rss_key @key = User.current.rss_key
respond_to do |format|
format.html
format.api
end
end end
def settings def settings
@@ -177,37 +158,23 @@ class ProjectsController < ApplicationController
@wiki ||= @project.wiki @wiki ||= @project.wiki
end end
# Edit @project
def edit def edit
end if request.post?
@project.attributes = params[:project]
# TODO: convert to PUT only
verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project redirect_to :action => 'settings', :id => @project
}
format.api { head :ok }
end
else else
respond_to do |format|
format.html {
settings settings
render :action => 'settings' render :action => 'settings'
}
format.api { render_validation_errors(@project) }
end end
end end
end end
verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed }
def modules def modules
@project.enabled_module_names = params[:enabled_module_names] @project.enabled_module_names = params[:enabled_modules]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project, :tab => 'modules' redirect_to :action => 'settings', :id => @project, :tab => 'modules'
end end
@@ -228,22 +195,197 @@ class ProjectsController < ApplicationController
# Delete @project # Delete @project
def destroy def destroy
@project_to_destroy = @project @project_to_destroy = @project
if request.get? if request.post? and params[:confirm]
# display confirmation view
else
if api_request? || params[:confirm]
@project_to_destroy.destroy @project_to_destroy.destroy
respond_to do |format| redirect_to :controller => 'admin', :action => 'projects'
format.html { redirect_to :controller => 'admin', :action => 'projects' }
format.api { head :ok }
end
end
end end
# hide project in layout # hide project in layout
@project = nil @project = nil
end end
# Add a new issue category to @project
def add_issue_category
@category = @project.issue_categories.build(params[:category])
if request.post?
if @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_category_id",
content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
}
end
end
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
end
end
end
end
end
# Add a new version to @project
def add_version
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'versions', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_fixed_version_id",
content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
}
end
end
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
end
end
end
end
def add_file
if request.post?
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = attach_files(container, params[:attachments])
if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments)
end
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end
@versions = @project.versions.sort
end
def save_activities
if request.post? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def reset_activities
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def list_files
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
def roadmap
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
@versions = @project.shared_versions.sort
@versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
conditions = {:tracker_id => @selected_tracker_ids}
if !@project.versions.include?(version)
conditions.merge!(:project_id => project_ids)
end
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => conditions,
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
end
def activity
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [events.first, User.current])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
private private
# Find project of id params[:id]
# if not found, redirect to project list
# Used as a before_filter
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project def find_optional_project
return true unless params[:id] return true unless params[:id]
@project = Project.find(params[:id]) @project = Project.find(params[:id])
@@ -252,6 +394,14 @@ private
render_404 render_404
end end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
end
end
# Validates parent_id param according to user's permissions # Validates parent_id param according to user's permissions
# TODO: move it to Project model in a validation that depends on User.current # TODO: move it to Project model in a validation that depends on User.current
def validate_parent_id def validate_parent_id
+9 -8
View File
@@ -25,12 +25,13 @@ class QueriesController < ApplicationController
@query.project = params[:query_is_for_all] ? nil : @project @query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current @query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
@query.group_by ||= params[:group_by]
@query.column_names = params[:c] if params[:c]
@query.column_names = nil if params[:default_columns] @query.column_names = nil if params[:default_columns]
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.group_by ||= params[:group_by]
if request.post? && params[:confirm] && @query.save if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
@@ -42,12 +43,12 @@ class QueriesController < ApplicationController
def edit def edit
if request.post? if request.post?
@query.filters = {} @query.filters = {}
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f] params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query] @query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all] @query.project = nil if params[:query_is_for_all]
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.group_by ||= params[:group_by]
@query.column_names = params[:c] if params[:c]
@query.column_names = nil if params[:default_columns] @query.column_names = nil if params[:default_columns]
if @query.save if @query.save
@@ -73,7 +74,7 @@ private
def find_optional_project def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id] @project = Project.find(params[:project_id]) if params[:project_id]
render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true) User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+164 -59
View File
@@ -17,79 +17,184 @@
class ReportsController < ApplicationController class ReportsController < ApplicationController
menu_item :issues menu_item :issues
before_filter :find_project, :authorize, :find_issue_statuses before_filter :find_project, :authorize
def issue_report def issue_report
@statuses = IssueStatus.find(:all, :order => 'position')
case params[:detail]
when "tracker"
@field = "tracker_id"
@rows = @project.trackers
@data = issues_by_tracker
@report_title = l(:field_tracker)
render :template => "reports/issue_report_details"
when "version"
@field = "fixed_version_id"
@rows = @project.shared_versions.sort
@data = issues_by_version
@report_title = l(:field_version)
render :template => "reports/issue_report_details"
when "priority"
@field = "priority_id"
@rows = IssuePriority.all
@data = issues_by_priority
@report_title = l(:field_priority)
render :template => "reports/issue_report_details"
when "category"
@field = "category_id"
@rows = @project.issue_categories
@data = issues_by_category
@report_title = l(:field_category)
render :template => "reports/issue_report_details"
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = issues_by_assigned_to
@report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details"
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = issues_by_author
@report_title = l(:field_author)
render :template => "reports/issue_report_details"
when "subproject"
@field = "project_id"
@rows = @project.descendants.active
@data = issues_by_subproject
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
else
@trackers = @project.trackers @trackers = @project.trackers
@versions = @project.shared_versions.sort @versions = @project.shared_versions.sort
@priorities = IssuePriority.all @priorities = IssuePriority.all
@categories = @project.issue_categories @categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }.sort @assignees = @project.members.collect { |m| m.user }.sort
@authors = @project.members.collect { |m| m.user }.sort @authors = @project.members.collect { |m| m.user }.sort
@subprojects = @project.descendants.visible @subprojects = @project.descendants.active
issues_by_tracker
@issues_by_tracker = Issue.by_tracker(@project) issues_by_version
@issues_by_version = Issue.by_version(@project) issues_by_priority
@issues_by_priority = Issue.by_priority(@project) issues_by_category
@issues_by_category = Issue.by_category(@project) issues_by_assigned_to
@issues_by_assigned_to = Issue.by_assigned_to(@project) issues_by_author
@issues_by_author = Issue.by_author(@project) issues_by_subproject
@issues_by_subproject = Issue.by_subproject(@project) || []
render :template => "reports/issue_report" render :template => "reports/issue_report"
end end
def issue_report_details
case params[:detail]
when "tracker"
@field = "tracker_id"
@rows = @project.trackers
@data = Issue.by_tracker(@project)
@report_title = l(:field_tracker)
when "version"
@field = "fixed_version_id"
@rows = @project.shared_versions.sort
@data = Issue.by_version(@project)
@report_title = l(:field_version)
when "priority"
@field = "priority_id"
@rows = IssuePriority.all
@data = Issue.by_priority(@project)
@report_title = l(:field_priority)
when "category"
@field = "category_id"
@rows = @project.issue_categories
@data = Issue.by_category(@project)
@report_title = l(:field_category)
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = Issue.by_assigned_to(@project)
@report_title = l(:field_assigned_to)
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = Issue.by_author(@project)
@report_title = l(:field_author)
when "subproject"
@field = "project_id"
@rows = @project.descendants.visible
@data = Issue.by_subproject(@project) || []
@report_title = l(:field_subproject)
end
respond_to do |format|
if @field
format.html {}
else
format.html { redirect_to :action => 'issue_report', :id => @project }
end
end
end end
private private
# Find project of id params[:id]
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue_statuses def issues_by_tracker
@statuses = IssueStatus.find(:all, :order => 'position') @issues_by_tracker ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
t.id as tracker_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
where
i.status_id=s.id
and i.tracker_id=t.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, t.id")
end
def issues_by_version
@issues_by_version ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
v.id as fixed_version_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
where
i.status_id=s.id
and i.fixed_version_id=v.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, v.id")
end
def issues_by_priority
@issues_by_priority ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
p.id as priority_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssuePriority.table_name} p
where
i.status_id=s.id
and i.priority_id=p.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, p.id")
end
def issues_by_category
@issues_by_category ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
c.id as category_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
where
i.status_id=s.id
and i.category_id=c.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, c.id")
end
def issues_by_assigned_to
@issues_by_assigned_to ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as assigned_to_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.assigned_to_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_author
@issues_by_author ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as author_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.author_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_subproject
@issues_by_subproject ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
i.project_id as project_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s
where
i.status_id=s.id
and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
@issues_by_subproject ||= []
end end
end end
+22 -75
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -24,45 +24,26 @@ class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController class RepositoriesController < ApplicationController
menu_item :repository menu_item :repository
menu_item :settings, :only => :edit
default_search_scope :changesets default_search_scope :changesets
before_filter :find_repository, :except => :edit before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit before_filter :find_project, :only => :edit
before_filter :authorize before_filter :authorize
accept_rss_auth :revisions accept_key_auth :revisions
rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
def edit def edit
@repository = @project.repository @repository = @project.repository
if !@repository && !params[:repository_scm].blank? if !@repository
@repository = Repository.factory(params[:repository_scm]) @repository = Repository.factory(params[:repository_scm])
@repository.project = @project if @repository @repository.project = @project if @repository
end end
if request.post? && @repository if request.post? && @repository
p1 = params[:repository] @repository.attributes = params[:repository]
p = {}
p_extra = {}
p1.each do |k, v|
if k =~ /^extra_/
p_extra[k] = v
else
p[k] = v
end
end
@repository.attributes = p
@repository.merge_extra_info(p_extra)
@repository.save @repository.save
end end
render(:update) do |page| render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
page.replace_html "tab-content-repository",
:partial => 'projects/settings/repository'
if @repository && !@project.repository
@project.reload # needed to reload association
page.replace_html "main-menu", render_main_menu(@project)
end
end
end end
def committers def committers
@@ -82,17 +63,13 @@ class RepositoriesController < ApplicationController
def destroy def destroy
@repository.destroy @repository.destroy
redirect_to :controller => 'projects', redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
:action => 'settings',
:id => @project,
:tab => 'repository'
end end
def show def show
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty? @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
@entries = @repository.entries(@path, @rev) @entries = @repository.entries(@path, @rev)
@changeset = @repository.find_changeset_by_name(@rev)
if request.xhr? if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true) @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else else
@@ -110,7 +87,6 @@ class RepositoriesController < ApplicationController
(show_error_not_found; return) unless @entry (show_error_not_found; return) unless @entry
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i) @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
@properties = @repository.properties(@path, @rev) @properties = @repository.properties(@path, @rev)
@changeset = @repository.find_changeset_by_name(@rev)
end end
def revisions def revisions
@@ -138,46 +114,24 @@ class RepositoriesController < ApplicationController
@content = @repository.cat(@path, @rev) @content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content (show_error_not_found; return) unless @content
if 'raw' == params[:format] || if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
! is_entry_text_data?(@content, @path)
# Force the download # Force the download
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) } send_data @content, :filename => @path.split('/').last
send_type = Redmine::MimeType.of(@path)
send_opt[:type] = send_type.to_s if send_type
send_data @content, send_opt
else else
# Prevent empty lines when displaying a file with Windows style eol # Prevent empty lines when displaying a file with Windows style eol
# TODO: UTF-16
# Is this needs? AttachmentsController reads file simply.
@content.gsub!("\r\n", "\n") @content.gsub!("\r\n", "\n")
@changeset = @repository.find_changeset_by_name(@rev)
end end
end end
def is_entry_text_data?(ent, path)
# UTF-16 contains "\x00".
# It is very strict that file contains less than 30% of ascii symbols
# in non Western Europe.
return true if Redmine::MimeType.is_type?('text', path)
# Ruby 1.8.6 has a bug of integer divisions.
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
return false if ent.is_binary_data?
true
end
private :is_entry_text_data?
def annotate def annotate
@entry = @repository.entry(@path, @rev) @entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry (show_error_not_found; return) unless @entry
@annotate = @repository.scm.annotate(@path, @rev) @annotate = @repository.scm.annotate(@path, @rev)
(render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty? (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
@changeset = @repository.find_changeset_by_name(@rev)
end end
def revision def revision
raise ChangesetNotFound if @rev.blank?
@changeset = @repository.find_changeset_by_name(@rev) @changeset = @repository.find_changeset_by_name(@rev)
raise ChangesetNotFound unless @changeset raise ChangesetNotFound unless @changeset
@@ -207,16 +161,12 @@ class RepositoriesController < ApplicationController
User.current.pref[:diff_type] = @diff_type User.current.pref[:diff_type] = @diff_type
User.current.preference.save User.current.preference.save
end end
@cache_key = "repositories/diff/#{@repository.id}/" +
Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}") @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
unless read_fragment(@cache_key) unless read_fragment(@cache_key)
@diff = @repository.diff(@path, @rev, @rev_to) @diff = @repository.diff(@path, @rev, @rev_to)
show_error_not_found unless @diff show_error_not_found unless @diff
end end
@changeset = @repository.find_changeset_by_name(@rev)
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
@diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
end end
end end
@@ -240,8 +190,11 @@ class RepositoriesController < ApplicationController
end end
private private
def find_project
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i @project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_repository def find_repository
@project = Project.find(params[:id]) @project = Project.find(params[:id])
@@ -251,12 +204,6 @@ class RepositoriesController < ApplicationController
@path ||= '' @path ||= ''
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
@rev_to = params[:rev_to] @rev_to = params[:rev_to]
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
if @repository.branches.blank?
raise InvalidRevisionParam
end
end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
rescue InvalidRevisionParam rescue InvalidRevisionParam
@@ -264,7 +211,7 @@ class RepositoriesController < ApplicationController
end end
def show_error_not_found def show_error_not_found
render_error :message => l(:error_scm_not_found), :status => 404 render_error l(:error_scm_not_found)
end end
# Handler for Redmine::Scm::Adapters::CommandFailed exception # Handler for Redmine::Scm::Adapters::CommandFailed exception
@@ -276,15 +223,11 @@ class RepositoriesController < ApplicationController
@date_to = Date.today @date_to = Date.today
@date_from = @date_to << 11 @date_from = @date_to << 11
@date_from = Date.civil(@date_from.year, @date_from.month, 1) @date_from = Date.civil(@date_from.year, @date_from.month, 1)
commits_by_day = repository.changesets.count( commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
:all, :group => :commit_date,
:conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
commits_by_month = [0] * 12 commits_by_month = [0] * 12
commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last } commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
changes_by_day = repository.changes.count( changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
:all, :group => :commit_date,
:conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
changes_by_month = [0] * 12 changes_by_month = [0] * 12
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
@@ -345,16 +288,20 @@ class RepositoriesController < ApplicationController
:graph_title => l(:label_commits_per_author), :graph_title => l(:label_commits_per_author),
:show_graph_title => true :show_graph_title => true
) )
graph.add_data( graph.add_data(
:data => commits_data, :data => commits_data,
:title => l(:label_revision_plural) :title => l(:label_revision_plural)
) )
graph.add_data( graph.add_data(
:data => changes_data, :data => changes_data,
:title => l(:label_change_plural) :title => l(:label_change_plural)
) )
graph.burn graph.burn
end end
end end
class Date class Date
+10 -7
View File
@@ -21,11 +21,16 @@ class RolesController < ApplicationController
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ], verify :method => :post, :only => [ :destroy, :move ],
:redirect_to => { :action => :index } :redirect_to => { :action => :list }
def index def index
list
render :action => 'list' unless request.xhr?
end
def list
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position' @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
render :action => "index", :layout => false if request.xhr? render :action => "list", :layout => false if request.xhr?
end end
def new def new
@@ -38,20 +43,18 @@ class RolesController < ApplicationController
end end
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index' redirect_to :action => 'index'
else end
@permissions = @role.setable_permissions @permissions = @role.setable_permissions
@roles = Role.find :all, :order => 'builtin, position' @roles = Role.find :all, :order => 'builtin, position'
end end
end
def edit def edit
@role = Role.find(params[:id]) @role = Role.find(params[:id])
if request.post? and @role.update_attributes(params[:role]) if request.post? and @role.update_attributes(params[:role])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'index'
else
@permissions = @role.setable_permissions
end end
@permissions = @role.setable_permissions
end end
def destroy def destroy
@@ -59,7 +62,7 @@ class RolesController < ApplicationController
@role.destroy @role.destroy
redirect_to :action => 'index' redirect_to :action => 'index'
rescue rescue
flash[:error] = l(:error_can_not_remove_role) flash[:error] = 'This role is in use and can not be deleted.'
redirect_to :action => 'index' redirect_to :action => 'index'
end end
+9 -7
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -24,8 +24,8 @@ class SearchController < ApplicationController
def index def index
@question = params[:q] || "" @question = params[:q] || ""
@question.strip! @question.strip!
@all_words = params[:all_words] ? params[:all_words].present? : true @all_words = params[:all_words] || (params[:submit] ? false : true)
@titles_only = params[:titles_only] ? params[:titles_only].present? : false @titles_only = !params[:titles_only].nil?
projects_to_search = projects_to_search =
case params[:scope] case params[:scope]
@@ -43,12 +43,12 @@ class SearchController < ApplicationController
begin; offset = params[:offset].to_time if params[:offset]; rescue; end begin; offset = params[:offset].to_time if params[:offset]; rescue; end
# quick jump to an issue # quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1.to_i) if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
redirect_to :controller => "issues", :action => "show", :id => $1 redirect_to :controller => "issues", :action => "show", :id => $1
return return
end end
@object_types = Redmine::Search.available_search_types.dup @object_types = %w(issues news documents changesets wiki_pages messages projects)
if projects_to_search.is_a? Project if projects_to_search.is_a? Project
# don't search projects # don't search projects
@object_types.delete('projects') @object_types.delete('projects')
@@ -68,13 +68,15 @@ class SearchController < ApplicationController
if !@tokens.empty? if !@tokens.empty?
# no more than 5 tokens to search for # no more than 5 tokens to search for
@tokens.slice! 5..-1 if @tokens.size > 5 @tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
@results = [] @results = []
@results_by_type = Hash.new {|h,k| h[k] = 0} @results_by_type = Hash.new {|h,k| h[k] = 0}
limit = 10 limit = 10
@scope.each do |s| @scope.each do |s|
r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search, r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
:all_words => @all_words, :all_words => @all_words,
:titles_only => @titles_only, :titles_only => @titles_only,
:limit => (limit+1), :limit => (limit+1),
+6 -9
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -26,7 +26,7 @@ class SettingsController < ApplicationController
end end
def edit def edit
@notifiables = Redmine::Notifiable.all @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated)
if request.post? && params[:settings] && params[:settings].is_a?(Hash) if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value| settings.each do |name, value|
@@ -36,16 +36,14 @@ class SettingsController < ApplicationController
end end
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :tab => params[:tab] redirect_to :action => 'edit', :tab => params[:tab]
else return
end
@options = {} @options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
@deliveries = ActionMailer::Base.perform_deliveries @deliveries = ActionMailer::Base.perform_deliveries
@guessed_host_and_path = request.host_with_port.dup @guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
Redmine::Themes.rescan
end
end end
def plugin def plugin
@@ -54,10 +52,9 @@ class SettingsController < ApplicationController
Setting["plugin_#{@plugin.id}"] = params[:settings] Setting["plugin_#{@plugin.id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => @plugin.id redirect_to :action => 'plugin', :id => @plugin.id
else end
@partial = @plugin.settings[:partial] @partial = @plugin.settings[:partial]
@settings = Setting["plugin_#{@plugin.id}"] @settings = Setting["plugin_#{@plugin.id}"]
end
rescue Redmine::PluginNotFound rescue Redmine::PluginNotFound
render_404 render_404
end end
+2 -3
View File
@@ -20,8 +20,7 @@ class SysController < ActionController::Base
def projects def projects
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
# extra_info attribute from repository breaks activeresource client render :xml => p.to_xml(:include => :repository)
render :xml => p.to_xml(:only => [:id, :identifier, :name, :is_public, :status], :include => {:repository => {:only => [:id, :url]}})
end end
def create_project_repository def create_project_repository
@@ -32,7 +31,7 @@ class SysController < ActionController::Base
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}." logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
project.repository = Repository.factory(params[:vendor], params[:repository]) project.repository = Repository.factory(params[:vendor], params[:repository])
if project.repository && project.repository.save if project.repository && project.repository.save
render :xml => project.repository.to_xml(:only => [:id, :url]), :status => 201 render :xml => project.repository, :status => 201
else else
render :nothing => true, :status => 422 render :nothing => true, :status => 422
end end
@@ -1,209 +0,0 @@
class TimeEntryReportsController < ApplicationController
menu_item :issues
before_filter :find_optional_project
before_filter :load_available_criterias
helper :sort
include SortHelper
helper :issues
helper :timelog
include TimelogHelper
helper :custom_fields
include CustomFieldsHelper
def report
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << time_report_joins
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@hours.each do |row|
case @columns
when 'year'
row['year'] = row['tyear']
when 'month'
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
private
# TODO: duplicated in TimelogController
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
end
# Retrieves the date range based on predefined ranges or specific from/to param dates
# TODO: duplicated in TimelogController
def retrieve_date_range
@free_period = false
@from, @to = nil, nil
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
case params[:period].to_s
when 'today'
@from = @to = Date.today
when 'yesterday'
@from = @to = Date.today - 1
when 'current_week'
@from = Date.today - (Date.today.cwday - 1)%7
@to = @from + 6
when 'last_week'
@from = Date.today - 7 - (Date.today.cwday - 1)%7
@to = @from + 6
when '7_days'
@from = Date.today - 7
@to = Date.today
when 'current_month'
@from = Date.civil(Date.today.year, Date.today.month, 1)
@to = (@from >> 1) - 1
when 'last_month'
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
@to = (@from >> 1) - 1
when '30_days'
@from = Date.today - 30
@to = Date.today
when 'current_year'
@from = Date.civil(Date.today.year, 1, 1)
@to = Date.civil(Date.today.year, 12, 31)
end
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
@free_period = true
else
# default
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
end
def load_available_criterias
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
@available_criterias
end
def time_report_joins
sql = ''
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
# TODO: rename hook
call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
sql
end
end
+160 -176
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,13 +17,10 @@
class TimelogController < ApplicationController class TimelogController < ApplicationController
menu_item :issues menu_item :issues
before_filter :find_project, :only => [:new, :create] before_filter :find_project, :authorize, :only => [:edit, :destroy]
before_filter :find_time_entry, :only => [:show, :edit, :update] before_filter :find_optional_project, :only => [:report, :details]
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
before_filter :authorize, :except => [:index] verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
before_filter :find_optional_project, :only => [:index]
accept_rss_auth :index
accept_api_auth :index, :show, :create, :update, :destroy
helper :sort helper :sort
include SortHelper include SortHelper
@@ -32,7 +29,129 @@ class TimelogController < ApplicationController
helper :custom_fields helper :custom_fields
include CustomFieldsHelper include CustomFieldsHelper
def index def report
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@hours.each do |row|
case @columns
when 'year'
row['year'] = row['tyear']
when 'month'
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
def details
sort_init 'spent_on', 'desc' sort_init 'spent_on', 'desc'
sort_update 'spent_on' => 'spent_on', sort_update 'spent_on' => 'spent_on',
'user' => 'user_id', 'user' => 'user_id',
@@ -42,42 +161,35 @@ class TimelogController < ApplicationController
'hours' => 'hours' 'hours' => 'hours'
cond = ARCondition.new cond = ARCondition.new
if @issue if @project.nil?
cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}" cond << Project.allowed_to_condition(User.current, :view_time_entries)
elsif @project elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?) cond << @project.project_condition(Setting.display_subprojects_issues?)
else
cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
end end
retrieve_date_range retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to] cond << ['spent_on BETWEEN ? AND ?', @from, @to]
TimeEntry.visible_by(User.current) do
respond_to do |format| respond_to do |format|
format.html { format.html {
# Paginate results # Paginate results
@entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions) @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page'] @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.visible.find(:all, @entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}], :include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions, :conditions => cond.conditions,
:order => sort_clause, :order => sort_clause,
:limit => @entry_pages.items_per_page, :limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset) :offset => @entry_pages.current.offset)
@total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
render :layout => !request.xhr? render :layout => !request.xhr?
} }
format.api {
@entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
@offset, @limit = api_offset_and_limit
@entries = TimeEntry.visible.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @limit,
:offset => @offset)
}
format.atom { format.atom {
entries = TimeEntry.visible.find(:all, entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}], :include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions, :conditions => cond.conditions,
:order => "#{TimeEntry.table_name}.created_on DESC", :order => "#{TimeEntry.table_name}.created_on DESC",
@@ -86,7 +198,7 @@ class TimelogController < ApplicationController
} }
format.csv { format.csv {
# Export all entries # Export all entries
@entries = TimeEntry.visible.find(:all, @entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions, :conditions => cond.conditions,
:order => sort_clause) :order => sort_clause)
@@ -94,163 +206,42 @@ class TimelogController < ApplicationController
} }
end end
end end
def show
respond_to do |format|
# TODO: Implement html response
format.html { render :nothing => true, :status => 406 }
format.api
end
end
def new
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
render :action => 'edit'
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
}
format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
end
end end
def edit def edit
@time_entry.attributes = params[:time_entry] (render_403; return) if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@time_entry.attributes = params[:time_entry] @time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save if request.post? and @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project redirect_back_or_default :action => 'details', :project_id => @time_entry.project
} return
format.api { head :ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
end end
end end
def bulk_edit
@available_activities = TimeEntryActivity.shared.active
@custom_fields = TimeEntry.first.available_custom_fields
end
def bulk_update
attributes = parse_params_for_bulk_time_entry_attributes(params)
unsaved_time_entry_ids = []
@time_entries.each do |time_entry|
time_entry.reload
time_entry.attributes = attributes
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
unless time_entry.save
# Keep unsaved time_entry ids to display them in flash error
unsaved_time_entry_ids << time_entry.id
end
end
set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy def destroy
@time_entries.each do |t| (render_404; return) unless @time_entry
begin (render_403; return) unless @time_entry.editable_by?(User.current)
unless t.destroy && t.destroyed? @time_entry.destroy
respond_to do |format|
format.html {
flash[:error] = l(:notice_unable_delete_time_entry)
redirect_to :back
}
format.api { render_validation_errors(t) }
end
return
end
rescue ::ActionController::RedirectBackError
redirect_to :action => 'index', :project_id => @projects.first
return
end
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_delete) flash[:notice] = l(:notice_successful_delete)
redirect_back_or_default(:action => 'index', :project_id => @projects.first) redirect_to :back
} rescue ::ActionController::RedirectBackError
format.api { head :ok } redirect_to :action => 'details', :project_id => @time_entry.project
end
end end
private private
def find_time_entry
@time_entry = TimeEntry.find(params[:id])
unless @time_entry.editable_by?(User.current)
render_403
return false
end
@project = @time_entry.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_time_entries
@time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @time_entries.empty?
@projects = @time_entries.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
if unsaved_time_entry_ids.empty?
flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
else
flash[:error] = l(:notice_failed_to_save_time_entries,
:count => unsaved_time_entry_ids.size,
:total => time_entries.size,
:ids => '#' + unsaved_time_entry_ids.join(', #'))
end
end
def find_project def find_project
if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present? if params[:id]
@issue = Issue.find(issue_id) @time_entry = TimeEntry.find(params[:id])
@project = @time_entry.project
elsif params[:issue_id]
@issue = Issue.find(params[:issue_id])
@project = @issue.project @project = @issue.project
elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present? elsif params[:project_id]
@project = Project.find(project_id) @project = Project.find(params[:project_id])
else else
render_404 render_404
return false return false
@@ -311,14 +302,7 @@ private
end end
@from, @to = @to, @from if @from && @to && @from > @to @from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today) @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today) @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
end
def parse_params_for_bulk_time_entry_attributes(params)
attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
attributes
end end
end end
+12 -7
View File
@@ -20,11 +20,16 @@ class TrackersController < ApplicationController
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => :destroy, :redirect_to => { :action => :index }
def index def index
list
render :action => 'list' unless request.xhr?
end
verify :method => :post, :only => :destroy, :redirect_to => { :action => :list }
def list
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
render :action => "index", :layout => false if request.xhr? render :action => "list", :layout => false if request.xhr?
end end
def new def new
@@ -35,7 +40,7 @@ class TrackersController < ApplicationController
@tracker.workflows.copy(copy_from) @tracker.workflows.copy(copy_from)
end end
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index' redirect_to :action => 'list'
return return
end end
@trackers = Tracker.find :all, :order => 'position' @trackers = Tracker.find :all, :order => 'position'
@@ -46,7 +51,7 @@ class TrackersController < ApplicationController
@tracker = Tracker.find(params[:id]) @tracker = Tracker.find(params[:id])
if request.post? and @tracker.update_attributes(params[:tracker]) if request.post? and @tracker.update_attributes(params[:tracker])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'list'
return return
end end
@projects = Project.find(:all) @projects = Project.find(:all)
@@ -55,10 +60,10 @@ class TrackersController < ApplicationController
def destroy def destroy
@tracker = Tracker.find(params[:id]) @tracker = Tracker.find(params[:id])
unless @tracker.issues.empty? unless @tracker.issues.empty?
flash[:error] = l(:error_can_not_delete_tracker) flash[:error] = "This tracker contains issues and can\'t be deleted."
else else
@tracker.destroy @tracker.destroy
end end
redirect_to :action => 'index' redirect_to :action => 'list'
end end
end end
+43 -137
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -19,8 +19,6 @@ class UsersController < ApplicationController
layout 'admin' layout 'admin'
before_filter :require_admin, :except => :show before_filter :require_admin, :except => :show
before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
accept_api_auth :index, :show, :create, :update, :destroy
helper :sort helper :sort
include SortHelper include SortHelper
@@ -31,16 +29,6 @@ class UsersController < ApplicationController
sort_init 'login', 'asc' sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on) sort_update %w(login firstname lastname mail admin created_on last_login_on)
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
scope = User
scope = scope.in_group(params[:group_id].to_i) if params[:group_id].present?
@status = params[:status] ? params[:status].to_i : 1 @status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
@@ -49,154 +37,91 @@ class UsersController < ApplicationController
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name] c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
end end
@user_count = scope.count(:conditions => c.conditions) @user_count = User.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count, @limit, params['page'] @user_pages = Paginator.new self, @user_count,
@offset ||= @user_pages.current.offset per_page_option,
@users = scope.find :all, params['page']
:order => sort_clause, @users = User.find :all,:order => sort_clause,
:conditions => c.conditions, :conditions => c.conditions,
:limit => @limit, :limit => @user_pages.items_per_page,
:offset => @offset :offset => @user_pages.current.offset
respond_to do |format|
format.html {
@groups = Group.all.sort
render :layout => !request.xhr? render :layout => !request.xhr?
}
format.api
end
end end
def show def show
# show projects based on current user visibility @user = User.active.find(params[:id])
@memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current)) @custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10) events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date) @events_by_day = events.group_by(&:event_date)
unless User.current.admin? if @user != User.current && !User.current.admin? && @memberships.empty? && events.empty?
if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
render_404 render_404
return return
end end
render :layout => 'base'
rescue ActiveRecord::RecordNotFound
render_404
end end
respond_to do |format| def add
format.html { render :layout => 'base' } if request.get?
format.api @user = User.new(:language => Setting.default_language)
end else
end @user = User.new(params[:user])
def new
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@auth_sources = AuthSource.find(:all)
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@user.safe_attributes = params[:user]
@user.admin = params[:user][:admin] || false @user.admin = params[:user][:admin] || false
@user.login = params[:user][:login] @user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
# TODO: Similar to My#account
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save if @user.save
@user.pref.save Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
Mailer.deliver_account_information(@user, params[:user][:password]) if params[:send_information]
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ? redirect_to(params[:continue] ? {:controller => 'users', :action => 'add'} :
{:controller => 'users', :action => 'new'} : {:controller => 'users', :action => 'edit', :id => @user})
{:controller => 'users', :action => 'edit', :id => @user} return
) end
}
format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
end end
else
@auth_sources = AuthSource.find(:all) @auth_sources = AuthSource.find(:all)
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@user) }
end
end
end end
def edit def edit
@auth_sources = AuthSource.find(:all) @user = User.find(params[:id])
@membership ||= Member.new if request.post?
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@user.admin = params[:user][:admin] if params[:user][:admin] @user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login] @user.login = params[:user][:login] if params[:user][:login]
if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?) @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
end @user.attributes = params[:user]
@user.safe_attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change) # Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
# TODO: Similar to My#account
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
if was_activated if was_activated
Mailer.deliver_account_activated(@user) Mailer.deliver_account_activated(@user)
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil? elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
Mailer.deliver_account_information(@user, params[:user][:password]) Mailer.deliver_account_information(@user, params[:password])
end end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :back redirect_to :back
}
format.api { head :ok }
end end
else end
@auth_sources = AuthSource.find(:all) @auth_sources = AuthSource.find(:all)
@membership ||= Member.new @membership ||= Member.new
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => :edit }
format.api { render_validation_errors(@user) }
end
end
rescue ::ActionController::RedirectBackError rescue ::ActionController::RedirectBackError
redirect_to :controller => 'users', :action => 'edit', :id => @user redirect_to :controller => 'users', :action => 'edit', :id => @user
end end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to(users_url) }
format.api { head :ok }
end
end
def edit_membership def edit_membership
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user) @user = User.find(params[:id])
@membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user)
@membership.attributes = params[:membership]
@membership.save if request.post? @membership.save if request.post?
respond_to do |format| respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js { format.js {
render(:update) {|page| render(:update) {|page|
@@ -204,17 +129,11 @@ class UsersController < ApplicationController
page.visual_effect(:highlight, "member-#{@membership.id}") page.visual_effect(:highlight, "member-#{@membership.id}")
} }
} }
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end end
end end
def destroy_membership def destroy_membership
@user = User.find(params[:id])
@membership = Member.find(params[:membership_id]) @membership = Member.find(params[:membership_id])
if request.post? && @membership.deletable? if request.post? && @membership.deletable?
@membership.destroy @membership.destroy
@@ -224,17 +143,4 @@ class UsersController < ApplicationController
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} } format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
end end
end end
private
def find_user
if params[:id] == 'current'
require_login || return
@user = User.current
else
@user = User.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+12 -95
View File
@@ -17,122 +17,41 @@
class VersionsController < ApplicationController class VersionsController < ApplicationController
menu_item :roadmap menu_item :roadmap
model_object Version before_filter :find_version, :except => :close_completed
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed] before_filter :find_project, :only => :close_completed
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
before_filter :find_project, :only => [:index, :new, :create, :close_completed]
before_filter :authorize before_filter :authorize
helper :custom_fields helper :custom_fields
helper :projects helper :projects
def index
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
@versions = @project.shared_versions || []
@versions += @project.rolled_up_versions.visible if @with_subprojects
@versions = @versions.uniq.sort
@versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
end
def show def show
@issues = @version.fixed_issues.visible.find(:all,
:include => [:status, :tracker, :priority],
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
end
def new
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
end
def create
# TODO: refactor with code above in #new
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_fixed_version_id",
content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
}
end
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
end
end
end
end end
def edit def edit
end if request.post? && params[:version]
def update
if request.put? && params[:version]
attributes = params[:version].dup attributes = params[:version].dup
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing']) attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
if @version.update_attributes(attributes) if @version.update_attributes(attributes)
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
else
respond_to do |format|
format.html { render :action => 'edit' }
end
end end
end end
end end
def close_completed def close_completed
if request.put? if request.post?
@project.close_completed_versions @project.close_completed_versions
end end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end end
def destroy def destroy
if @version.fixed_issues.empty?
@version.destroy @version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
else rescue
flash[:error] = l(:notice_unable_delete_version) flash[:error] = l(:notice_unable_delete_version)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end end
end
def status_by def status_by
respond_to do |format| respond_to do |format|
@@ -142,18 +61,16 @@ class VersionsController < ApplicationController
end end
private private
def find_version
@version = Version.find(params[:id])
@project = @version.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project def find_project
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
end
end
end end
+13 -5
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -76,13 +76,21 @@ private
def set_watcher(user, watching) def set_watcher(user, watching)
@watched.set_watcher(user, watching) @watched.set_watcher(user, watching)
if params[:replace].present?
if params[:replace].is_a? Array
replace_ids = params[:replace]
else
replace_ids = [params[:replace]]
end
else
replace_ids = 'watcher'
end
respond_to do |format| respond_to do |format|
format.html { redirect_to :back } format.html { redirect_to :back }
format.js do format.js do
render(:update) do |page| render(:update) do |page|
c = watcher_css(@watched) replace_ids.each do |replace_id|
page.select(".#{c}").each do |item| page.replace_html replace_id, watcher_link(@watched, user, :replace => replace_ids)
page.replace_html item, watcher_link(@watched, user)
end end
end end
end end
+46 -93
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,46 +17,23 @@
require 'diff' require 'diff'
# The WikiController follows the Rails REST controller pattern but with
# a few differences
#
# * index - shows a list of WikiPages grouped by page or date
# * new - not used
# * create - not used
# * show - will also show the form for creating a new wiki page
# * edit - used to edit an existing or new page
# * update - used to save a wiki page update to the database, including new pages
# * destroy - normal
#
# Other member and collection methods are also used
#
# TODO: still being worked on
class WikiController < ApplicationController class WikiController < ApplicationController
default_search_scope :wiki_pages default_search_scope :wiki_pages
before_filter :find_wiki, :authorize before_filter :find_wiki, :authorize
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy] before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
helper :attachments helper :attachments
include AttachmentsHelper include AttachmentsHelper
helper :watchers helper :watchers
# List of pages, sorted alphabetically and by parent (hierarchy)
def index
load_pages_for_index
@pages_by_parent_id = @pages.group_by(&:parent_id)
end
# List of page, by last update
def date_index
load_pages_for_index
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
end
# display a page (in editing mode if it doesn't exist) # display a page (in editing mode if it doesn't exist)
def show def index
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record? if @page.new_record?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? if User.current.allowed_to?(:edit_wiki_pages, @project)
edit edit
render :action => 'edit' render :action => 'edit'
else else
@@ -70,7 +47,6 @@ class WikiController < ApplicationController
return return
end end
@content = @page.content_for_version(params[:version]) @content = @page.content_for_version(params[:version])
if User.current.allowed_to?(:export_wiki_pages, @project)
if params[:format] == 'html' if params[:format] == 'html'
export = render_to_string :action => 'export', :layout => false export = render_to_string :action => 'export', :layout => false
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
@@ -79,13 +55,13 @@ class WikiController < ApplicationController
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return return
end end
end
@editable = editable? @editable = editable?
render :action => 'show' render :action => 'show'
end end
# edit an existing page or a new one # edit an existing page or a new one
def edit def edit
@page = @wiki.find_or_new_page(params[:page])
return render_403 unless editable? return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record? @page.content = WikiContent.new(:page => @page) if @page.new_record?
@@ -93,45 +69,28 @@ class WikiController < ApplicationController
@content.text = initial_page_content(@page) if @content.text.blank? @content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment # don't keep previous comment
@content.comments = nil @content.comments = nil
if request.get?
# To prevent StaleObjectError exception when reverting to a previous version # To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version @content.version = @page.content.version
end else
if !@page.new_record? && @content.text == params[:content][:text]
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
# Creates a new page or updates an existing one
def update
return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
# don't save if text wasn't changed # don't save if text wasn't changed
redirect_to :action => 'show', :project_id => @project, :id => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
return return
end end
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.attributes = params[:content] @content.attributes = params[:content]
@content.author = User.current @content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record # if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save) if (@page.new_record? ? @page.save : @content.save)
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'show', :project_id => @project, :id => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
else end
render :action => 'edit'
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
# Optimistic locking exception # Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict) flash[:error] = l(:notice_locking_conflict)
render :action => 'edit'
end end
# rename a page # rename a page
@@ -142,14 +101,13 @@ class WikiController < ApplicationController
@original_title = @page.pretty_title @original_title = @page.pretty_title
if request.post? && @page.update_attributes(params[:wiki_page]) if request.post? && @page.update_attributes(params[:wiki_page])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :project_id => @project, :id => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
end end
end end
verify :method => :post, :only => :protect, :redirect_to => { :action => :show }
def protect def protect
@page.update_attribute :protected, params[:protected] @page.update_attribute :protected, params[:protected]
redirect_to :action => 'show', :project_id => @project, :id => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
end end
# show page history # show page history
@@ -176,7 +134,6 @@ class WikiController < ApplicationController
render_404 unless @annotate render_404 unless @annotate
end end
verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
# Removes a wiki page and its history # Removes a wiki page and its history
# Children can be either set as root pages, removed or reassigned to another parent page # Children can be either set as root pages, removed or reassigned to another parent page
def destroy def destroy
@@ -203,22 +160,37 @@ class WikiController < ApplicationController
end end
end end
@page.destroy @page.destroy
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end end
# Export wiki to a single html file # display special pages
def export def special
if User.current.allowed_to?(:export_wiki_pages, @project) page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
when 'page_index', 'date_index'
# eager load information about last updates, without loading text
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
@pages_by_parent_id = @pages.group_by(&:parent_id)
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title' @pages = @wiki.pages.find :all, :order => 'title'
export = render_to_string :action => 'export_multiple', :layout => false export = render_to_string :action => 'export_multiple', :layout => false
send_data(export, :type => 'text/html', :filename => "wiki.html") send_data(export, :type => 'text/html', :filename => "wiki.html")
return
else else
redirect_to :action => 'show', :project_id => @project, :id => nil # requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil
return
end end
render :action => "special_#{page_title}"
end end
def preview def preview
page = @wiki.find_page(params[:id]) page = @wiki.find_page(params[:page])
# page is nil when previewing a new page # page is nil when previewing a new page
return render_403 unless page.nil? || editable?(page) return render_403 unless page.nil? || editable?(page)
if page if page
@@ -231,39 +203,24 @@ class WikiController < ApplicationController
def add_attachment def add_attachment
return render_403 unless editable? return render_403 unless editable?
attachments = Attachment.attach_files(@page, params[:attachments]) attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page) redirect_to :action => 'index', :page => @page.title
redirect_to :action => 'show', :id => @page.title, :project_id => @project
end end
private private
def find_wiki def find_wiki
@project = Project.find(params[:project_id]) @project = Project.find(params[:id])
@wiki = @project.wiki @wiki = @project.wiki
render_404 unless @wiki render_404 unless @wiki
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
# Finds the requested page or a new page if it doesn't exist
def find_existing_or_new_page
@page = @wiki.find_or_new_page(params[:id])
if @wiki.page_found_with_redirect?
redirect_to params.update(:id => @page.title)
end
end
# Finds the requested page and returns a 404 error if it doesn't exist # Finds the requested page and returns a 404 error if it doesn't exist
def find_existing_page def find_existing_page
@page = @wiki.find_page(params[:id]) @page = @wiki.find_page(params[:page])
if @page.nil? render_404 if @page.nil?
render_404
return
end
if @wiki.page_found_with_redirect?
redirect_to params.update(:id => @page.title)
end
end end
# Returns true if the current user is allowed to edit the page, otherwise false # Returns true if the current user is allowed to edit the page, otherwise false
@@ -277,8 +234,4 @@ private
extend helper unless self.instance_of?(helper) extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page) helper.instance_method(:initial_page_content).bind(self).call(page)
end end
def load_pages_for_index
@pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
end
end end
+7
View File
@@ -34,4 +34,11 @@ class WikisController < ApplicationController
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
end end
end end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+7 -26
View File
@@ -19,8 +19,6 @@ class WorkflowsController < ApplicationController
layout 'admin' layout 'admin'
before_filter :require_admin before_filter :require_admin
before_filter :find_roles
before_filter :find_trackers
def index def index
@workflow_counts = Workflow.count_by_tracker_and_role @workflow_counts = Workflow.count_by_tracker_and_role
@@ -32,36 +30,29 @@ class WorkflowsController < ApplicationController
if request.post? if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |status_id, transitions| (params[:issue_status] || []).each { |old, news|
transitions.each { |new_status_id, options| news.each { |new|
author = options.is_a?(Array) && options.include?('author') && !options.include?('always') @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
} }
} }
if @role.save if @role.save
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
return
end end
end end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true) @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
if @tracker && @used_statuses_only && @tracker.issue_statuses.any? if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
@statuses = @tracker.issue_statuses @statuses = @tracker.issue_statuses
end end
@statuses ||= IssueStatus.find(:all, :order => 'position') @statuses ||= IssueStatus.find(:all, :order => 'position')
if @tracker && @role && @statuses.any?
workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
@workflows = {}
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
@workflows['author'] = workflows.select {|w| w.author}
@workflows['assignee'] = workflows.select {|w| w.assignee}
end
end end
def copy def copy
@trackers = Tracker.find(:all, :order => 'position')
@roles = Role.find(:all, :order => 'builtin, position')
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any' if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
@source_tracker = nil @source_tracker = nil
@@ -89,14 +80,4 @@ class WorkflowsController < ApplicationController
end end
end end
end end
private
def find_roles
@roles = Role.find(:all, :order => 'builtin, position')
end
def find_trackers
@trackers = Tracker.find(:all, :order => 'position')
end
end end
+8
View File
@@ -20,4 +20,12 @@ module AdminHelper
options_for_select([[l(:label_all), ''], options_for_select([[l(:label_all), ''],
[l(:status_active), 1]], selected) [l(:status_active), 1]], selected)
end end
def css_project_classes(project)
s = 'project'
s << ' root' if project.root?
s << ' child' if project.child?
s << (project.leaf? ? ' leaf' : ' parent')
s
end
end end
+104 -326
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'coderay'
require 'coderay/helpers/file_type'
require 'forwardable' require 'forwardable'
require 'cgi' require 'cgi'
@@ -32,11 +34,6 @@ module ApplicationHelper
end end
# Display a link if user is authorized # Display a link if user is authorized
#
# @param [String] name Anchor text (passed to link_to)
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
# @param [optional, Hash] html_options Options passed to link_to
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end end
@@ -104,41 +101,8 @@ module ApplicationHelper
# * :text - Link text (default to the formatted revision) # * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={}) def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision) text = options.delete(:text) || format_revision(revision)
rev = revision.respond_to?(:identifier) ? revision.identifier : revision
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
:title => l(:label_revision_id, format_revision(revision)))
end
# Generates a link to a message
def link_to_message(message, options={}, html_options = nil)
link_to(
h(truncate(message.subject, :length => 60)),
{ :controller => 'messages', :action => 'show',
:board_id => message.board_id,
:id => message.root,
:r => (message.parent_id && message.id),
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
}.merge(options),
html_options
)
end
# Generates a link to a project if active
# Examples:
#
# link_to_project(project) # => link to the specified project overview
# link_to_project(project, :action=>'settings') # => link to project settings
# link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
#
def link_to_project(project, options={}, html_options = nil)
if project.active?
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
link_to(h(project), url, html_options)
else
h(project)
end
end end
def toggle_link(name, id, options={}) def toggle_link(name, id, options={})
@@ -187,15 +151,15 @@ module ApplicationHelper
end end
end end
def render_page_hierarchy(pages, node=nil, options={}) def render_page_hierarchy(pages, node=nil)
content = '' content = ''
if pages[node] if pages[node]
content << "<ul class=\"pages-hierarchy\">\n" content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page| pages[node].each do |page|
content << "<li>" content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}, content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
content << "</li>\n" content << "</li>\n"
end end
content << "</ul>\n" content << "</ul>\n"
@@ -223,8 +187,8 @@ module ApplicationHelper
# Renders the project quick-jump box # Renders the project quick-jump box
def render_project_jump_box def render_project_jump_box
return unless User.current.logged? # Retrieve them now to avoid a COUNT query
projects = User.current.memberships.collect(&:project).compact.uniq projects = User.current.projects.all
if projects.any? if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' + s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" + "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
@@ -241,12 +205,7 @@ module ApplicationHelper
s = '' s = ''
project_tree(projects) do |project, level| project_tree(projects) do |project, level|
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '') name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
tag_options = {:value => project.id} tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
tag_options[:selected] = 'selected'
else
tag_options[:selected] = nil
end
tag_options.merge!(yield(project)) if block_given? tag_options.merge!(yield(project)) if block_given?
s << content_tag('option', name_prefix + h(project), tag_options) s << content_tag('option', name_prefix + h(project), tag_options)
end end
@@ -254,10 +213,15 @@ module ApplicationHelper
end end
# Yields the given block for each project with its level in the tree # Yields the given block for each project with its level in the tree
#
# Wrapper for Project#project_tree
def project_tree(projects, &block) def project_tree(projects, &block)
Project.project_tree(projects, &block) ancestors = []
projects.sort_by(&:lft).each do |project|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield project, ancestors.size
ancestors << project
end
end end
def project_nested_ul(projects, &block) def project_nested_ul(projects, &block)
@@ -297,16 +261,6 @@ module ApplicationHelper
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
end end
# Truncates at line break after 250 characters or options[:length]
def truncate_lines(string, options={})
length = options[:length] || 250
if string.to_s =~ /\A(.{#{length}}.*?)$/m
"#{$1}..."
else
string
end
end
def html_hours(text) def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end end
@@ -318,14 +272,15 @@ module ApplicationHelper
def time_tag(time) def time_tag(time)
text = distance_of_time_in_words(Time.now, time) text = distance_of_time_in_words(Time.now, time)
if @project if @project
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time)) link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
else else
content_tag('acronym', text, :title => format_time(time)) content_tag('acronym', text, :title => format_time(time))
end end
end end
def syntax_highlight(name, content) def syntax_highlight(name, content)
Redmine::SyntaxHighlighting.highlight_by_filename(content, name) type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
end end
def to_path_param(path) def to_path_param(path)
@@ -334,35 +289,42 @@ module ApplicationHelper
def pagination_links_full(paginator, count=nil, options={}) def pagination_links_full(paginator, count=nil, options={})
page_param = options.delete(:page_param) || :page page_param = options.delete(:page_param) || :page
per_page_links = options.delete(:per_page_links)
url_param = params.dup url_param = params.dup
# don't reuse query params if filters are present
url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
html = '' html = ''
if paginator.current.previous if paginator.current.previous
html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' ' html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
end end
html << (pagination_links_each(paginator, options) do |n| html << (pagination_links_each(paginator, options) do |n|
link_to_content_update(n.to_s, url_param.merge(page_param => n)) link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
end || '') end || '')
if paginator.current.next if paginator.current.next
html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next)) html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
end end
unless count.nil? unless count.nil?
html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" html << [
if per_page_links != false && links = per_page_links(paginator.items_per_page) " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
html << " | #{links}" per_page_links(paginator.items_per_page)
end ].compact.join(' | ')
end end
html html
end end
def per_page_links(selected=nil) def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
links = Setting.per_page_options_array.collect do |n| links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) n == selected ? n : link_to_remote(n, {:update => "content",
:url => params.dup.merge(:per_page => n),
:method => :get},
{:href => url_for(url_param.merge(:per_page => n))})
end end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end end
@@ -390,15 +352,15 @@ module ApplicationHelper
h(Setting.app_title) h(Setting.app_title)
else else
b = [] b = []
ancestors = (@project.root? ? [] : @project.ancestors.visible.all) ancestors = (@project.root? ? [] : @project.ancestors.visible)
if ancestors.any? if ancestors.any?
root = ancestors.shift root = ancestors.shift
b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
if ancestors.size > 2 if ancestors.size > 2
b << '&#8230;' b << '&#8230;'
ancestors = ancestors[-2, 2] ancestors = ancestors[-2, 2]
end end
b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
end end
b << h(@project) b << h(@project)
b.join(' &#187; ') b.join(' &#187; ')
@@ -418,19 +380,6 @@ module ApplicationHelper
end end
end end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
css = []
if theme = Redmine::Themes.theme(Setting.ui_theme)
css << 'theme-' + theme.name
end
css << 'controller-' + params[:controller]
css << 'action-' + params[:action]
css.join(' ')
end
def accesskey(s) def accesskey(s)
Redmine::AccessKeys.key_for s Redmine::AccessKeys.key_for s
end end
@@ -447,82 +396,50 @@ module ApplicationHelper
text = args.shift text = args.shift
when 2 when 2
obj = args.shift obj = args.shift
attr = args.shift text = obj.send(args.shift).to_s
text = obj.send(attr).to_s
else else
raise ArgumentError, 'invalid arguments to textilizable' raise ArgumentError, 'invalid arguments to textilizable'
end end
return '' if text.blank? return '' if text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
if @parsed_headings.any?
replace_toc(text, @parsed_headings)
end
text
end
def parse_non_pre_blocks(text)
s = StringScanner.new(text)
tags = []
parsed = ''
while !s.eos?
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
if tags.empty?
yield text
end
parsed << text
if tag
if closing
if tags.last == tag.downcase
tags.pop
end
else
tags << tag.downcase
end
parsed << full_tag
end
end
# Close any non closing tags
while tag = tags.pop
parsed << "</#{tag}>"
end
parsed
end
def parse_inline_attachments(text, project, obj, attr, only_path, options)
# when using an image link, try to use an attachment, if possible # when using an image link, try to use an attachment, if possible
if options[:attachments] || (obj && obj.respond_to?(:attachments)) attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
attachments = nil
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| if attachments
filename, ext, alt, alttext = $1.downcase, $2, $3, $4 attachments = attachments.sort_by(&:created_on).reverse
attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
style = $1
filename = $6.downcase
# search for the picture in attachments # search for the picture in attachments
if found = attachments.detect { |att| att.filename.downcase == filename } if found = attachments.detect { |att| att.filename.downcase == filename }
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub('"', '') desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
if !desc.blank? && alttext.blank? alt = desc.blank? ? nil : "(#{desc})"
alt = " title=\"#{desc}\" alt=\"#{desc}\"" "!#{style}#{image_url}#{alt}!"
end
"src=\"#{image_url}\"#{alt}"
else else
m m
end end
end end
end end
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
# different methods for formatting wiki links
case options[:wiki_links]
when :local
# used for local links to html files
format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
when :anchor
# used for single-file wiki export
format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
else
format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
end end
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
# Wiki links # Wiki links
# #
# Examples: # Examples:
@@ -533,13 +450,12 @@ module ApplicationHelper
# [[project:|mytext]] # [[project:|mytext]]
# [[project:mypage]] # [[project:mypage]]
# [[project:mypage|mytext]] # [[project:mypage|mytext]]
def parse_wiki_links(text, project, obj, attr, only_path, options) text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
link_project = project link_project = project
esc, all, page, title = $1, $2, $3, $5 esc, all, page, title = $1, $2, $3, $5
if esc.nil? if esc.nil?
if page =~ /^([^\:]+)\:(.*)$/ if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_identifier($1) || Project.find_by_name($1) link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = $2 page = $2
title ||= $1 if page.blank? title ||= $1 if page.blank?
end end
@@ -550,21 +466,10 @@ module ApplicationHelper
if page =~ /^(.+?)\#(.+)$/ if page =~ /^(.+?)\#(.+)$/
page, anchor = $1, $2 page, anchor = $1, $2
end end
anchor = sanitize_anchor_name(anchor) if anchor.present?
# check if page exists # check if page exists
wiki_page = link_project.wiki.find_page(page) wiki_page = link_project.wiki.find_page(page)
url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
"##{anchor}" :class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
case options[:wiki_links]
when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
when :anchor; "##{title}" # used for single-file wiki export
else
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
end
end
link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
else else
# project or wiki doesn't exist # project or wiki doesn't exist
all all
@@ -573,7 +478,6 @@ module ApplicationHelper
all all
end end
end end
end
# Redmine links # Redmine links
# #
@@ -601,29 +505,18 @@ module ApplicationHelper
# export:some/file -> Force the download of the file # export:some/file -> Force the download of the file
# Forum messages: # Forum messages:
# message#1218 -> Link to message with id 1218 # message#1218 -> Link to message with id 1218
# text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
# Links can refer other objects from other projects, using project identifier: leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
# identifier:r52
# identifier:document:"Some document"
# identifier:version:1.0.0
# identifier:source:some/file
def parse_redmine_links(text, project, obj, attr, only_path, options)
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
link = nil link = nil
if project_identifier
project = Project.visible.find_by_identifier(project_identifier)
end
if esc.nil? if esc.nil?
if prefix.nil? && sep == 'r' if prefix.nil? && sep == 'r'
# project.changesets.visible raises an SQL error because of a double join on repositories if project && (changeset = project.changesets.find_by_revision(oid))
if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset', :class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100)) :title => truncate_single_line(changeset.comments, :length => 100))
end end
elsif sep == '#' elsif sep == '#'
oid = identifier.to_i oid = oid.to_i
case prefix case prefix
when nil when nil
if issue = Issue.visible.find_by_id(oid, :include => :status) if issue = Issue.visible.find_by_id(oid, :include => :status)
@@ -632,49 +525,51 @@ module ApplicationHelper
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
end end
when 'document' when 'document'
if document = Document.visible.find_by_id(oid) if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document' :class => 'document'
end end
when 'version' when 'version'
if version = Version.visible.find_by_id(oid) if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version' :class => 'version'
end end
when 'message' when 'message'
if message = Message.visible.find_by_id(oid, :include => :parent) if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
link = link_to_message(message, {:only_path => only_path}, :class => 'message') link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
end :controller => 'messages',
when 'project' :action => 'show',
if p = Project.visible.find_by_id(oid) :board_id => message.board,
link = link_to_project(p, {:only_path => only_path}, :class => 'project') :id => message.root,
:anchor => (message.parent ? "message-#{message.id}" : nil)},
:class => 'message'
end end
end end
elsif sep == ':' elsif sep == ':'
# removes the double quotes if any # removes the double quotes if any
name = identifier.gsub(%r{^"(.*)"$}, "\\1") name = oid.gsub(%r{^"(.*)"$}, "\\1")
case prefix case prefix
when 'document' when 'document'
if project && document = project.documents.visible.find_by_title(name) if project && document = project.documents.find_by_title(name)
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document' :class => 'document'
end end
when 'version' when 'version'
if project && version = project.versions.visible.find_by_name(name) if project && version = project.versions.find_by_name(name)
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version' :class => 'version'
end end
when 'commit' when 'commit'
if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset', :class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100) :title => truncate_single_line(changeset.comments, :length => 100)
end end
when 'source', 'export' when 'source', 'export'
if project && project.repository && User.current.allowed_to?(:browse_repository, project) if project && project.repository
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
path, rev, anchor = $1, $3, $5 path, rev, anchor = $1, $3, $5
link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
:path => to_path_param(path), :path => to_path_param(path),
:rev => rev, :rev => rev,
:anchor => anchor, :anchor => anchor,
@@ -682,69 +577,17 @@ module ApplicationHelper
:class => (prefix == 'export' ? 'source download' : 'source') :class => (prefix == 'export' ? 'source download' : 'source')
end end
when 'attachment' when 'attachment'
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments && attachment = attachments.detect {|a| a.filename == name } if attachments && attachment = attachments.detect {|a| a.filename == name }
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
:class => 'attachment' :class => 'attachment'
end end
when 'project'
if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
end end
end end
end end
end leading + (link || "#{prefix}#{sep}#{oid}")
leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
end
end end
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE) text
# Headings and TOC
# Adds ids and links to headings unless options[:headings] is set to false
def parse_headings(text, project, obj, attr, only_path, options)
return if options[:headings] == false
text.gsub!(HEADING_RE) do
level, attrs, content = $1.to_i, $2, $3
item = strip_tags(content).strip
anchor = sanitize_anchor_name(item)
@parsed_headings << [level, anchor, item]
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings)
text.gsub!(TOC_RE) do
if headings.empty?
''
else
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\"><li>"
root = headings.map(&:first).min
current = root
started = false
headings.each do |level, anchor, item|
if level > current
out << '<ul><li>' * (level - current)
elsif level < current
out << "</li></ul>\n" * (current - level) + "</li><li>"
elsif started
out << '</li><li>'
end
out << "<a href=\"##{anchor}\">#{item}</a>"
current = level
started = true
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
end
end
end end
# Same as Rails' simple_format helper without using paragraphs # Same as Rails' simple_format helper without using paragraphs
@@ -799,28 +642,6 @@ module ApplicationHelper
content_tag('p', legend, :class => 'pourcent') content_tag('p', legend, :class => 'pourcent')
end end
def checked_image(checked=true)
if checked
image_tag 'toggle_check.png'
end
end
def context_menu(url)
unless @context_menu_included
content_for :header_tags do
javascript_include_tag('context_menu') +
stylesheet_link_tag('context_menu')
end
if l(:direction) == 'rtl'
content_for :header_tags do
stylesheet_link_tag('context_menu_rtl')
end
end
@context_menu_included = true
end
javascript_tag "new ContextMenu('#{ url_for(url) }')"
end
def context_menu_link(name, url, options={}) def context_menu_link(name, url, options={})
options[:class] ||= '' options[:class] ||= ''
if options.delete(:selected) if options.delete(:selected)
@@ -852,8 +673,6 @@ module ApplicationHelper
'Calendar._FD = 1;' # Monday 'Calendar._FD = 1;' # Monday
when 7 when 7
'Calendar._FD = 0;' # Sunday 'Calendar._FD = 0;' # Sunday
when 6
'Calendar._FD = 6;' # Saturday
else else
'' # use language '' # use language
end end
@@ -881,7 +700,7 @@ module ApplicationHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { }) def avatar(user, options = { })
if Setting.gravatar_enabled? if Setting.gravatar_enabled?
options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default}) options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
email = nil email = nil
if user.respond_to?(:mail) if user.respond_to?(:mail)
email = user.mail email = user.mail
@@ -889,51 +708,6 @@ module ApplicationHelper
email = $1 email = $1
end end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
else
''
end
end
def sanitize_anchor_name(anchor)
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
end
# Returns the javascript tags that are included in the html layout head
def javascript_heads
tags = javascript_include_tag(:defaults)
unless User.current.pref.warn_on_leaving_unsaved == '0'
tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
end
tags
end
def favicon
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
end
def robot_exclusion_tag
'<meta name="robots" content="noindex,follow,noarchive" />'
end
# Returns true if arg is expected in the API response
def include_in_api_response?(arg)
unless @included_in_api_response
param = params[:include]
@included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
@included_in_api_response.collect!(&:strip)
end
@included_in_api_response.include?(arg.to_s)
end
# Returns options or nil if nometa param or X-Redmine-Nometa header
# was set in the request
def api_meta(options)
if params[:nometa].present? || request.headers['X-Redmine-Nometa']
# compatibility mode for activeresource clients that raise
# an error when unserializing an array with attributes
nil
else
options
end end
end end
@@ -945,7 +719,11 @@ module ApplicationHelper
return self return self
end end
def link_to_content_update(text, url_params = {}, html_options = {}) def link_to_remote_content_update(text, url_params)
link_to(text, url_params, html_options) link_to_remote(text,
{:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_params)}
)
end end
end end
+2 -14
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -29,18 +29,6 @@ module AttachmentsHelper
end end
def to_utf8(str) def to_utf8(str)
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
return str if str.valid_encoding?
else
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
end
begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str str
end end
end end
end
-37
View File
@@ -1,37 +0,0 @@
module CalendarsHelper
def link_to_previous_month(year, month, options={})
target_year, target_month = if month == 1
[year - 1, 12]
else
[year, month - 1]
end
name = if target_month == 12
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month(('&#171; ' + name), target_year, target_month, options)
end
def link_to_next_month(year, month, options={})
target_year, target_month = if month == 12
[year + 1, 1]
else
[year, month + 1]
end
name = if target_month == 1
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month((name + ' &#187;'), target_year, target_month, options)
end
def link_to_month(link_name, year, month, options={})
link_to_content_update(link_name, params.merge(:year => year, :month => month))
end
end
+20 -25
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -36,8 +36,7 @@ module CustomFieldsHelper
field_name = "#{name}[custom_field_values][#{custom_field.id}]" field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}" field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) case custom_field.field_format
case field_format.try(:edit_as)
when "date" when "date"
text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) + text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
calendar_for(field_id) calendar_for(field_id)
@@ -49,7 +48,7 @@ module CustomFieldsHelper
blank_option = custom_field.is_required? ? blank_option = custom_field.is_required? ?
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') : (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
'<option></option>' '<option></option>'
select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id) select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
else else
text_field_tag(field_name, custom_value.value, :id => field_id) text_field_tag(field_name, custom_value.value, :id => field_id)
end end
@@ -68,11 +67,10 @@ module CustomFieldsHelper
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end end
def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil) def custom_field_tag_for_bulk_edit(custom_field)
field_name = "#{name}[custom_field_values][#{custom_field.id}]" field_name = "custom_field_values[#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}" field_id = "custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) case custom_field.field_format
case field_format.try(:edit_as)
when "date" when "date"
text_field_tag(field_name, '', :id => field_id, :size => 10) + text_field_tag(field_name, '', :id => field_id, :size => 10) +
calendar_for(field_id) calendar_for(field_id)
@@ -83,7 +81,7 @@ module CustomFieldsHelper
[l(:general_text_yes), '1'], [l(:general_text_yes), '1'],
[l(:general_text_no), '0']]), :id => field_id) [l(:general_text_no), '0']]), :id => field_id)
when "list" when "list"
select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values_options(projects)), :id => field_id) select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
else else
text_field_tag(field_name, '', :id => field_id) text_field_tag(field_name, '', :id => field_id)
end end
@@ -97,22 +95,19 @@ module CustomFieldsHelper
# Return a string used to display a custom value # Return a string used to display a custom value
def format_value(value, field_format) def format_value(value, field_format)
Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy return "" unless value && !value.empty?
case field_format
when "date"
begin; format_date(value.to_date); rescue; value end
when "bool"
l(value == "1" ? :general_text_Yes : :general_text_No)
else
value
end
end end
# Return an array of custom field formats which can be used in select_tag # Return an array of custom field formats which can be used in select_tag
def custom_field_formats_for_select(custom_field) def custom_field_formats_for_select
Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name) CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
end
# Renders the custom_values in api views
def render_api_custom_values(custom_values, api)
api.array :custom_fields do
custom_values.each do |custom_value|
api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
api.value custom_value.value
end
end
end unless custom_values.empty?
end end
end end
-41
View File
@@ -1,41 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module GanttHelper
def gantt_zoom_link(gantt, in_or_out)
case in_or_out
when :in
if gantt.zoom < 4
link_to_content_update l(:text_zoom_in),
params.merge(gantt.params.merge(:zoom => (gantt.zoom+1))),
:class => 'icon icon-zoom-in'
else
content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in')
end
when :out
if gantt.zoom > 1
link_to_content_update l(:text_zoom_out),
params.merge(gantt.params.merge(:zoom => (gantt.zoom-1))),
:class => 'icon icon-zoom-out'
else
content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out')
end
end
end
end
-2
View File
@@ -1,2 +0,0 @@
module IssueMovesHelper
end
+29 -153
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -18,78 +18,19 @@
module IssuesHelper module IssuesHelper
include ApplicationHelper include ApplicationHelper
def issue_list(issues, &block)
ancestors = []
issues.each do |issue|
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield issue, ancestors.size
ancestors << issue unless issue.leaf?
end
end
# Renders a HTML/CSS tooltip
#
# To use, a trigger div is needed. This is a div with the class of "tooltip"
# that contains this method wrapped in a span with the class of "tip"
#
# <div class="tooltip"><%= link_to_issue(issue) %>
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
# </div>
#
def render_issue_tooltip(issue) def render_issue_tooltip(issue)
@cached_label_status ||= l(:field_status)
@cached_label_start_date ||= l(:field_start_date) @cached_label_start_date ||= l(:field_start_date)
@cached_label_due_date ||= l(:field_due_date) @cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to) @cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority) @cached_label_priority ||= l(:field_priority)
@cached_label_project ||= l(:field_project)
link_to_issue(issue) + "<br /><br />" + link_to_issue(issue) + "<br /><br />" +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
"<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" + "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" + "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" + "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}" "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end end
def issue_heading(issue)
h("#{issue.tracker} ##{issue.id}")
end
def render_issue_subject_with_tree(issue)
s = ''
ancestors = issue.root? ? [] : issue.ancestors.visible.all
ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor))
end
s << '<div>'
subject = h(issue.subject)
if issue.is_private?
subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
end
s << content_tag('h3', subject)
s << '</div>' * (ancestors.size + 1)
s
end
def render_descendants_tree(issue)
s = '<form><table class="list issues">'
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
s << content_tag('tr',
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
content_tag('td', h(child.status)) +
content_tag('td', link_to_user(child.assigned_to)) +
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
:class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
end
s << '</form></table>'
s
end
def render_custom_fields_rows(issue) def render_custom_fields_rows(issue)
return if issue.custom_field_values.empty? return if issue.custom_field_values.empty?
ordered_values = [] ordered_values = []
@@ -109,24 +50,6 @@ module IssuesHelper
s s
end end
def issues_destroy_confirmation_message(issues)
issues = [issues] unless issues.is_a?(Array)
message = l(:text_issues_destroy_confirmation)
descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
if descendant_count > 0
issues.each do |issue|
next if issue.root?
issues.each do |other_issue|
descendant_count -= 1 if issue.is_descendant_of?(other_issue)
end
end
if descendant_count > 0
message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
end
end
message
end
def sidebar_queries def sidebar_queries
unless @sidebar_queries unless @sidebar_queries
# User can see public queries and his own queries # User can see public queries and his own queries
@@ -134,58 +57,45 @@ module IssuesHelper
# Project specific queries and global queries # Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all, @sidebar_queries = Query.find(:all,
:select => 'id, name, is_public', :select => 'id, name',
:order => "name ASC", :order => "name ASC",
:conditions => visible.conditions) :conditions => visible.conditions)
end end
@sidebar_queries @sidebar_queries
end end
def query_links(title, queries)
# links to #index on issues/show
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
content_tag('h3', title) +
queries.collect {|query|
link_to(h(query.name), url_params.merge(:query_id => query))
}.join('<br />')
end
def render_sidebar_queries
out = ''
queries = sidebar_queries.select {|q| !q.is_public?}
out << query_links(l(:label_my_queries), queries) if queries.any?
queries = sidebar_queries.select {|q| q.is_public?}
out << query_links(l(:label_query_plural), queries) if queries.any?
out
end
def show_detail(detail, no_html=false) def show_detail(detail, no_html=false)
case detail.property case detail.property
when 'attr' when 'attr'
field = detail.prop_key.to_s.gsub(/\_id$/, "") label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
label = l(("field_" + field).to_sym) case detail.prop_key
case when 'due_date', 'start_date'
when ['due_date', 'start_date'].include?(detail.prop_key)
value = format_date(detail.value.to_date) if detail.value value = format_date(detail.value.to_date) if detail.value
old_value = format_date(detail.old_value.to_date) if detail.old_value old_value = format_date(detail.old_value.to_date) if detail.old_value
when 'project_id'
when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key) p = Project.find_by_id(detail.value) and value = p.name if detail.value
value = find_name_by_reflection(field, detail.value) p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
old_value = find_name_by_reflection(field, detail.old_value) when 'status_id'
s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
when detail.prop_key == 'estimated_hours' s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
when 'tracker_id'
t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
when 'assigned_to_id'
u = User.find_by_id(detail.value) and value = u.name if detail.value
u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
when 'priority_id'
e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
when 'category_id'
c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
when 'fixed_version_id'
v = Version.find_by_id(detail.value) and value = v.name if detail.value
v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
when 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank? value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
when detail.prop_key == 'parent_id'
label = l(:field_parent_issue)
value = "##{detail.value}" unless detail.value.blank?
old_value = "##{detail.old_value}" unless detail.old_value.blank?
when detail.prop_key == 'is_private'
value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
end end
when 'cf' when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key) custom_field = CustomField.find_by_id(detail.prop_key)
@@ -215,16 +125,7 @@ module IssuesHelper
end end
end end
if detail.property == 'attr' && detail.prop_key == 'description' if !detail.value.blank?
s = l(:text_journal_changed_no_detail, :label => label)
unless no_html
diff_link = link_to 'diff',
{:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id},
:title => l(:label_view_diff)
s << " (#{ diff_link })"
end
s
elsif !detail.value.blank?
case detail.property case detail.property
when 'attr', 'cf' when 'attr', 'cf'
if !detail.old_value.blank? if !detail.old_value.blank?
@@ -240,29 +141,6 @@ module IssuesHelper
end end
end end
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
association = Issue.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by_id(id)
return record.name if record
end
end
# Renders issue children recursively
def render_api_issue_children(issue, api)
return if issue.leaf?
api.array :children do
issue.children.each do |child|
api.issue(:id => child.id) do
api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
api.subject child.subject
render_api_issue_children(child, api)
end
end
end
end
def issues_to_csv(issues, project = nil) def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator) decimal_separator = l(:general_csv_decimal_separator)
@@ -282,7 +160,6 @@ module IssuesHelper
l(:field_due_date), l(:field_due_date),
l(:field_done_ratio), l(:field_done_ratio),
l(:field_estimated_hours), l(:field_estimated_hours),
l(:field_parent_issue),
l(:field_created_on), l(:field_created_on),
l(:field_updated_on) l(:field_updated_on)
] ]
@@ -309,7 +186,6 @@ module IssuesHelper
format_date(issue.due_date), format_date(issue.due_date),
issue.done_ratio, issue.done_ratio,
issue.estimated_hours.to_s.gsub('.', decimal_separator), issue.estimated_hours.to_s.gsub('.', decimal_separator),
issue.parent_id,
format_time(issue.created_on), format_time(issue.created_on),
format_time(issue.updated_on) format_time(issue.updated_on)
] ]
+3 -3
View File
@@ -16,13 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module JournalsHelper module JournalsHelper
def render_notes(issue, journal, options={}) def render_notes(journal, options={})
content = '' content = ''
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))) editable = journal.editable_by?(User.current)
links = [] links = []
if !journal.notes.blank? if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'), links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} }, { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
:title => l(:button_quote)) if options[:reply_links] :title => l(:button_quote)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal }, { :controller => 'journals', :action => 'edit', :id => journal },
+9
View File
@@ -16,4 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module MessagesHelper module MessagesHelper
def link_to_message(message)
return '' unless message
link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
:action => 'show',
:board_id => message.board_id,
:id => message.root,
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
end
end end
+4 -8
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -46,7 +46,7 @@ module ProjectsHelper
options = '' options = ''
options << "<option value=''></option>" if project.allowed_parents.include?(nil) options << "<option value=''></option>" if project.allowed_parents.include?(nil)
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected) options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
content_tag('select', options, :name => 'project[parent_id]', :id => 'project_parent_id') content_tag('select', options, :name => 'project[parent_id]')
end end
# Renders a tree of projects as a nested set of unordered lists # Renders a tree of projects as a nested set of unordered lists
@@ -56,10 +56,7 @@ module ProjectsHelper
s = '' s = ''
if projects.any? if projects.any?
ancestors = [] ancestors = []
original_project = @project
projects.each do |project| projects.each do |project|
# set the project environment to please macros.
@project = project
if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else else
@@ -72,13 +69,12 @@ module ProjectsHelper
end end
classes = (ancestors.empty? ? 'root' : 'child') classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" + s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank? s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
s << "</div>\n" s << "</div>\n"
ancestors << project ancestors << project
end end
s << ("</li></ul>\n" * ancestors.size) s << ("</li></ul>\n" * ancestors.size)
@project = original_project
end end
s s
end end
+3 -39
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -50,51 +50,15 @@ module QueriesHelper
when 'User' when 'User'
link_to_user value link_to_user value
when 'Project' when 'Project'
link_to_project value link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
when 'Version' when 'Version'
link_to(h(value), :controller => 'versions', :action => 'show', :id => value) link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
when 'TrueClass' when 'TrueClass'
l(:general_text_Yes) l(:general_text_Yes)
when 'FalseClass' when 'FalseClass'
l(:general_text_No) l(:general_text_No)
when 'Issue'
link_to_issue(value, :subject => false)
else else
h(value) h(value)
end end
end end
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
raise ::Unauthorized unless @query.visible?
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else
if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields] || params[:f]
@query.filters = {}
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
@query.group_by = params[:group_by]
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
@query.project = @project
end
end
end
end end
+2 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+36 -138
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,15 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv' require 'iconv'
require 'redmine/codeset_util'
module RepositoriesHelper module RepositoriesHelper
def format_revision(revision) def format_revision(txt)
if revision.respond_to? :format_identifier txt.to_s[0,8]
revision.format_identifier
else
revision.to_s
end
end end
def truncate_at_line_break(text, length = 255) def truncate_at_line_break(text, length = 255)
@@ -49,8 +44,7 @@ module RepositoriesHelper
when 'A' when 'A'
# Detects moved/copied files # Detects moved/copied files
if !change.from_path.blank? if !change.from_path.blank?
change.action = change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
@changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
end end
change change
when 'D' when 'D'
@@ -64,77 +58,56 @@ module RepositoriesHelper
changes.each do |change| changes.each do |change|
p = tree p = tree
dirs = change.path.to_s.split('/').select {|d| !d.blank?} dirs = change.path.to_s.split('/').select {|d| !d.blank?}
path = ''
dirs.each do |dir| dirs.each do |dir|
path += '/' + dir
p[:s] ||= {} p[:s] ||= {}
p = p[:s] p = p[:s]
p[path] ||= {} p[dir] ||= {}
p = p[path] p = p[dir]
end end
p[:c] = change p[:c] = change
end end
render_changes_tree(tree[:s]) render_changes_tree(tree[:s])
end end
def render_changes_tree(tree) def render_changes_tree(tree)
return '' if tree.nil? return '' if tree.nil?
output = '' output = ''
output << '<ul>' output << '<ul>'
tree.keys.sort.each do |file| tree.keys.sort.each do |file|
s = !tree[file][:s].nil?
c = tree[file][:c]
style = 'change' style = 'change'
text = File.basename(h(file)) style << ' folder' if s
if s = tree[file][:s] style << " change-#{c.action}" if c
style << ' folder'
path_param = to_path_param(@repository.relative_path(file)) text = h(file)
text = link_to(text, :controller => 'repositories', unless c.nil?
:action => 'show',
:id => @project,
:path => path_param,
:rev => @changeset.identifier)
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(s)
elsif c = tree[file][:c]
style << " change-#{c.action}"
path_param = to_path_param(@repository.relative_path(c.path)) path_param = to_path_param(@repository.relative_path(c.path))
text = link_to(text, :controller => 'repositories', text = link_to(text, :controller => 'repositories',
:action => 'entry', :action => 'entry',
:id => @project, :id => @project,
:path => path_param, :path => path_param,
:rev => @changeset.identifier) unless c.action == 'D' :rev => @changeset.revision) unless s || c.action == 'D'
text << " - #{c.revision}" unless c.revision.blank? text << " - #{c.revision}" unless c.revision.blank?
text << ' (' + link_to('diff', :controller => 'repositories', text << ' (' + link_to('diff', :controller => 'repositories',
:action => 'diff', :action => 'diff',
:id => @project, :id => @project,
:path => path_param, :path => path_param,
:rev => @changeset.identifier) + ') ' if c.action == 'M' :rev => @changeset.revision) + ') ' if c.action == 'M'
text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank? text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
output << "<li class='#{style}'>#{text}</li>"
end end
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(tree[file][:s]) if s
end end
output << '</ul>' output << '</ul>'
output output
end end
def to_utf8(str) def to_utf8(str)
return str if str.nil?
str = to_utf8_internal(str)
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
end
str
end
def to_utf8_internal(str)
return str if str.nil?
if str.respond_to?(:force_encoding)
str.force_encoding('ASCII-8BIT')
end
return str if str.empty?
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
end
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip) @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@encodings.each do |encoding| @encodings.each do |encoding|
begin begin
@@ -143,37 +116,24 @@ module RepositoriesHelper
# do nothing here and try the next encoding # do nothing here and try the next encoding
end end
end end
str = Redmine::CodesetUtil.replace_invalid_utf8(str) str
end end
private :to_utf8_internal
def repository_field_tags(form, repository) def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags" method = repository.class.name.demodulize.underscore + "_field_tags"
if repository.is_a?(Repository) && send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
respond_to?(method) && method != 'repository_field_tags'
send(method, form, repository)
end
end end
def scm_select_tag(repository) def scm_select_tag(repository)
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']] scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
Redmine::Scm::Base.all.each do |scm| REDMINE_SUPPORTED_SCM.each do |scm|
if Setting.enabled_scm.include?(scm) || scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
(repository && repository.class.name.demodulize == scm)
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
end
end end
select_tag('repository_scm', select_tag('repository_scm',
options_for_select(scm_options, repository.class.name.demodulize), options_for_select(scm_options, repository.class.name.demodulize),
:disabled => (repository && !repository.new_record?), :disabled => (repository && !repository.new_record?),
:onchange => remote_function( :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
:url => {
:controller => 'repositories',
:action => 'edit',
:id => @project
},
:method => :get,
:with => "Form.serialize(this.form)")
) )
end end
@@ -186,99 +146,37 @@ module RepositoriesHelper
end end
def subversion_field_tags(form, repository) def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
:disabled => (repository && !repository.root_url.blank?)) +
'<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') + '<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
content_tag('p', form.text_field(:login, :size => 30)) + content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field( content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='repository[password]';", :onfocus => "this.value=''; this.name='repository[password]';",
:onchange => "this.name='repository[password]';")) :onchange => "this.name='repository[password]';"))
end end
def darcs_field_tags(form, repository) def darcs_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
:url, :label => l(:field_path_to_repository),
:size => 60, :required => true,
:disabled => (repository && !repository.new_record?))) +
content_tag('p', form.select(
:log_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_commit_logs_encoding), :required => true))
end end
def mercurial_field_tags(form, repository) def mercurial_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
:url, :label => l(:field_path_to_repository),
:size => 60, :required => true,
:disabled => (repository && !repository.root_url.blank?)
) +
'<br />' + l(:text_mercurial_repository_note)) +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_scm_path_encoding)
) +
'<br />' + l(:text_scm_path_encoding_note))
end end
def git_field_tags(form, repository) def git_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
:url, :label => l(:field_path_to_repository),
:size => 60, :required => true,
:disabled => (repository && !repository.root_url.blank?)
) +
'<br />' + l(:text_git_repository_note)) +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_scm_path_encoding)
) +
'<br />' + l(:text_scm_path_encoding_note)) +
content_tag('p', form.check_box(
:extra_report_last_commit,
:label => l(:label_git_report_last_commit)
))
end end
def cvs_field_tags(form, repository) def cvs_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
:root_url, content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
:label => l(:field_cvsroot),
:size => 60, :required => true,
:disabled => !repository.new_record?)) +
content_tag('p', form.text_field(
:url,
:label => l(:field_cvs_module),
:size => 30, :required => true,
:disabled => !repository.new_record?)) +
content_tag('p', form.select(
:log_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_commit_logs_encoding), :required => true)) +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_scm_path_encoding)
) +
'<br />' + l(:text_scm_path_encoding_note))
end end
def bazaar_field_tags(form, repository) def bazaar_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
:url, :label => l(:field_path_to_repository),
:size => 60, :required => true,
:disabled => (repository && !repository.new_record?))) +
content_tag('p', form.select(
:log_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_commit_logs_encoding), :required => true))
end end
def filesystem_field_tags(form, repository) def filesystem_field_tags(form, repository)
content_tag('p', form.text_field( content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
:url, :label => l(:field_root_directory),
:size => 60, :required => true,
:disabled => (repository && !repository.root_url.blank?))) +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => l(:field_scm_path_encoding)
) +
'<br />' + l(:text_scm_path_encoding_note))
end end
end end
+4 -4
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -39,7 +39,7 @@ module SearchHelper
end end
def type_label(t) def type_label(t)
l("label_#{t.singularize}_plural", :default => t.to_s.humanize) l("label_#{t.singularize}_plural")
end end
def project_select_tag def project_select_tag
@@ -57,7 +57,7 @@ module SearchHelper
c = results_by_type[t] c = results_by_type[t]
next if c == 0 next if c == 0
text = "#{type_label(t)} (#{c})" text = "#{type_label(t)} (#{c})"
links << link_to(text, :q => params[:q], :titles_only => params[:titles_only], :all_words => params[:all_words], :scope => params[:scope], t => 1) links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
end end
('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty? ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
end end
+4 -21
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -33,9 +33,7 @@ module SettingsHelper
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
end end
setting_label(setting, options) + setting_label(setting, options) +
select_tag("settings[#{setting}]", select_tag("settings[#{setting}]", options_for_select(choices, Setting.send(setting).to_s), options)
options_for_select(choices, Setting.send(setting).to_s),
options)
end end
def setting_multiselect(setting, choices, options={}) def setting_multiselect(setting, choices, options={})
@@ -46,13 +44,8 @@ module SettingsHelper
hidden_field_tag("settings[#{setting}][]", '') + hidden_field_tag("settings[#{setting}][]", '') +
choices.collect do |choice| choices.collect do |choice|
text, value = (choice.is_a?(Array) ? choice : [choice, choice]) text, value = (choice.is_a?(Array) ? choice : [choice, choice])
content_tag( content_tag('label',
'label', check_box_tag("settings[#{setting}][]", value, Setting.send(setting).include?(value)) + text.to_s,
check_box_tag(
"settings[#{setting}][]",
value,
Setting.send(setting).include?(value)
) + text.to_s,
:class => 'block' :class => 'block'
) )
end.join end.join
@@ -78,14 +71,4 @@ module SettingsHelper
label = options.delete(:label) label = options.delete(:label)
label != false ? content_tag("label", l(label || "setting_#{setting}")) : '' label != false ? content_tag("label", l(label || "setting_#{setting}")) : ''
end end
# Renders a notification field for a Redmine::Notifiable option
def notification_field(notifiable)
return content_tag(:label,
check_box_tag('settings[notified_events][]',
notifiable.name,
Setting.notified_events.include?(notifiable.name)) +
l_or_humanize(notifiable.name, :prefix => 'label_'),
:class => notifiable.parent.present? ? "parent" : '')
end
end end
+7 -12
View File
@@ -81,7 +81,7 @@ module SortHelper
def to_sql def to_sql
sql = @criteria.collect do |k,o| sql = @criteria.collect do |k,o|
if s = @available_criteria[k] if s = @available_criteria[k]
(o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ') (o ? s.to_a : s.to_a.collect {|c| "#{c} DESC"}).join(', ')
end end
end.compact.join(', ') end.compact.join(', ')
sql.blank? ? nil : sql sql.blank? ? nil : sql
@@ -120,15 +120,6 @@ module SortHelper
@criteria.slice!(3) @criteria.slice!(3)
self self
end end
# Appends DESC to the sort criterion unless it has a fixed order
def append_desc(criterion)
if criterion =~ / (asc|desc)$/i
criterion
else
"#{criterion} DESC"
end
end
end end
def sort_name def sort_name
@@ -200,12 +191,16 @@ module SortHelper
caption = column.to_s.humanize unless caption caption = column.to_s.humanize unless caption
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
url_options = params.merge(sort_options) # don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
# Add project_id to url_options # Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
link_to_content_update(caption, url_options, :class => css) link_to_remote(caption,
{:update => "content", :url => url_options, :method => :get},
{:href => url_for(url_options),
:class => css})
end end
# Returns a table header <th> tag with a sort link for the named column # Returns a table header <th> tag with a sort link for the named column
+1 -1
View File
@@ -57,7 +57,7 @@ module TimelogHelper
if value.to_s.empty? if value.to_s.empty?
data.select {|row| row[criteria].blank? } data.select {|row| row[criteria].blank? }
else else
data.select {|row| row[criteria].to_s == value.to_s} data.select {|row| row[criteria] == value}
end end
end end
+4 -8
View File
@@ -33,19 +33,15 @@ module UsersHelper
options options
end end
def user_mail_notification_options(user)
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
end
def change_status_link(user) def change_status_link(user)
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} url = {:controller => 'users', :action => 'edit', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
if user.locked? if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered? elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user != User.current elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end end
end end
+5 -4
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,10 +17,11 @@
module VersionsHelper module VersionsHelper
STATUS_BY_CRITERIAS = %w(category tracker status priority author assigned_to) STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to)
def render_issue_status_by(version, criteria) def render_issue_status_by(version, criteria)
criteria = 'category' unless STATUS_BY_CRITERIAS.include?(criteria) criteria ||= 'category'
raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria)
h = Hash.new {|k,v| k[v] = [0, 0]} h = Hash.new {|k,v| k[v] = [0, 0]}
begin begin
+21 -18
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,17 +17,27 @@
module WatchersHelper module WatchersHelper
def watcher_tag(object, user, options={}) # Valid options
content_tag("span", watcher_link(object, user), :class => watcher_css(object)) # * :id - the element id
# * :replace - a string or array of element ids that will be
# replaced
def watcher_tag(object, user, options={:replace => 'watcher'})
id = options[:id]
id ||= options[:replace] if options[:replace].is_a? String
content_tag("span", watcher_link(object, user, options), :id => id)
end end
def watcher_link(object, user) # Valid options
# * :replace - a string or array of element ids that will be
# replaced
def watcher_link(object, user, options={:replace => 'watcher'})
return '' unless user && user.logged? && object.respond_to?('watched_by?') return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user) watched = object.watched_by?(user)
url = {:controller => 'watchers', url = {:controller => 'watchers',
:action => (watched ? 'unwatch' : 'watch'), :action => (watched ? 'unwatch' : 'watch'),
:object_type => object.class.to_s.underscore, :object_type => object.class.to_s.underscore,
:object_id => object.id} :object_id => object.id,
:replace => options[:replace]}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
{:url => url}, {:url => url},
:href => url_for(url), :href => url_for(url),
@@ -35,16 +45,11 @@ module WatchersHelper
end end
# Returns the css class used to identify watch links for a given +object+
def watcher_css(object)
"#{object.class.to_s.underscore}-#{object.id}-watcher"
end
# Returns a comma separated list of users watching the given object # Returns a comma separated list of users watching the given object
def watchers_list(object) def watchers_list(object)
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project) remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
lis = object.watcher_users.collect do |user| object.watcher_users.collect do |user|
s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s s = content_tag('span', link_to_user(user), :class => 'user')
if remove_allowed if remove_allowed
url = {:controller => 'watchers', url = {:controller => 'watchers',
:action => 'destroy', :action => 'destroy',
@@ -54,11 +59,9 @@ module WatchersHelper
s += ' ' + link_to_remote(image_tag('delete.png'), s += ' ' + link_to_remote(image_tag('delete.png'),
{:url => url}, {:url => url},
:href => url_for(url), :href => url_for(url),
:style => "vertical-align: middle", :style => "vertical-align: middle")
:class => "delete")
end end
"<li>#{ s }</li>" s
end end.join(",\n")
lis.empty? ? "" : "<ul>#{ lis.join("\n") }</ul>"
end end
end end
+41 -7
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -18,18 +18,52 @@
module WikiHelper module WikiHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0) def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
s = '' s = ''
if pages.has_key?(parent) pages.select {|p| p.parent == parent}.each do |page|
pages[parent].each do |page|
attrs = "value='#{page.id}'" attrs = "value='#{page.id}'"
attrs << " selected='selected'" if selected == page attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" + s << "<option value='#{page.id}'>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1) wiki_page_options_for_select(pages, selected, page, level + 1)
end end
end
s s
end end
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
words_add = 0
words_del = 0
dels = 0
del_off = 0
wdiff.diff.diffs.each do |diff|
add_at = nil
add_to = nil
del_at = nil
deleted = ""
diff.each do |change|
pos = change[1]
if change[0] == "+"
add_at = pos + dels unless add_at
add_to = pos + dels
words_add += 1
else
del_at = pos unless del_at
deleted << ' ' + h(change[2])
words_del += 1
end
end
if add_at
words[add_at] = '<span class="diff_in">' + words[add_at]
words[add_to] = words[add_to] + '</span>'
end
if del_at
words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
dels += 1
del_off += words_del
words_del = 0
end
end
simple_format_without_paragraph(words.join(' '))
end
end end
+8 -39
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -43,7 +43,7 @@ class Attachment < ActiveRecord::Base
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
cattr_accessor :storage_path cattr_accessor :storage_path
@@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files" @@storage_path = "#{RAILS_ROOT}/files"
def validate def validate
if self.filesize > Setting.attachment_max_size.to_i.kilobytes if self.filesize > Setting.attachment_max_size.to_i.kilobytes
@@ -134,33 +134,6 @@ class Attachment < ActiveRecord::Base
File.readable?(diskfile) File.readable?(diskfile)
end end
# Bulk attaches a set of files to an object
#
# Returns a Hash of the results:
# :files => array of the attached files
# :unsaved => array of the files that could not be attached
def self.attach_files(obj, attachments)
attached = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
if a.new_record?
obj.unsaved_attachments ||= []
obj.unsaved_attachments << a
else
attached << a
end
end
end
{:files => attached, :unsaved => obj.unsaved_attachments}
end
private private
def sanitize_filename(value) def sanitize_filename(value)
# get only the filename, not the whole path # get only the filename, not the whole path
@@ -174,18 +147,14 @@ private
# Returns an ASCII or hashed filename # Returns an ASCII or hashed filename
def self.disk_filename(filename) def self.disk_filename(filename)
timestamp = DateTime.now.strftime("%y%m%d%H%M%S") df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
ascii = ''
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
ascii = filename df << filename
else else
ascii = Digest::MD5.hexdigest(filename) df << Digest::MD5.hexdigest(filename)
# keep the extension if any # keep the extension if any
ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$} df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
end end
while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}")) df
timestamp.succ!
end
"#{timestamp}_#{ascii}"
end end
end end
-19
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSource < ActiveRecord::Base class AuthSource < ActiveRecord::Base
include Redmine::Ciphering
has_many :users has_many :users
validates_presence_of :name validates_presence_of :name
@@ -34,23 +32,6 @@ class AuthSource < ActiveRecord::Base
"Abstract" "Abstract"
end end
def account_password
read_ciphered_attribute(:account_password)
end
def account_password=(arg)
write_ciphered_attribute(:account_password, arg)
end
def allow_password_changes?
self.class.allow_password_changes?
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
# Try to authenticate a user not yet registered against available sources # Try to authenticate a user not yet registered against available sources
def self.authenticate(login, password) def self.authenticate(login, password)
AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source| AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
+25 -57
View File
@@ -20,8 +20,8 @@ require 'iconv'
class AuthSourceLdap < AuthSource class AuthSourceLdap < AuthSource
validates_presence_of :host, :port, :attr_login validates_presence_of :host, :port, :attr_login
validates_length_of :name, :host, :maximum => 60, :allow_nil => true validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true
validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
validates_numericality_of :port, :only_integer => true validates_numericality_of :port, :only_integer => true
@@ -33,12 +33,30 @@ class AuthSourceLdap < AuthSource
def authenticate(login, password) def authenticate(login, password)
return nil if login.blank? || password.blank? return nil if login.blank? || password.blank?
attrs = get_user_dn(login) attrs = []
# get user's DN
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) ldap_con = initialize_ldap_con(self.account, self.account_password)
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
return attrs.except(:dn) object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
dn = String.new
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
# only ask for the DN if on-the-fly registration is disabled
:attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
dn = entry.dn
attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
:auth_source_id => self.id ] if onthefly_register?
end end
return nil if dn.empty?
logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
# authenticate user
ldap_con = initialize_ldap_con(dn, password)
return nil unless ldap_con.bind
# return user's attributes
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
attrs
rescue Net::LDAP::LdapError => text rescue Net::LDAP::LdapError => text
raise "LdapError: " + text raise "LdapError: " + text
end end
@@ -72,56 +90,6 @@ class AuthSourceLdap < AuthSource
Net::LDAP.new options Net::LDAP.new options
end end
def get_user_attributes_from_ldap_entry(entry)
{
:dn => entry.dn,
:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
:auth_source_id => self.id
}
end
# Return the attributes needed for the LDAP search. It will only
# include the user attributes if on-the-fly registration is enabled
def search_attributes
if onthefly_register?
['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
else
['dn']
end
end
# Check if a DN (user record) authenticates with the password
def authenticate_dn(dn, password)
if dn.present? && password.present?
initialize_ldap_con(dn, password).bind
end
end
# Get the user's dn and any attributes for them, given their login
def get_user_dn(login)
ldap_con = initialize_ldap_con(self.account, self.account_password)
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
attrs = {}
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
:attributes=> search_attributes) do |entry|
if onthefly_register?
attrs = get_user_attributes_from_ldap_entry(entry)
else
attrs = {:dn => entry.dn}
end
logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
end
attrs
end
def self.get_attr(entry, attr_name) def self.get_attr(entry, attr_name)
if !attr_name.blank? if !attr_name.blank?
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
+1 -1
View File
@@ -18,7 +18,7 @@
class Board < ActiveRecord::Base class Board < ActiveRecord::Base
belongs_to :project belongs_to :project
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC" has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
acts_as_list :scope => :project_id acts_as_list :scope => :project_id
acts_as_watchable acts_as_watchable
+2 -12
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -19,18 +19,8 @@ class Change < ActiveRecord::Base
belongs_to :changeset belongs_to :changeset
validates_presence_of :changeset_id, :action, :path validates_presence_of :changeset_id, :action, :path
before_save :init_path
def relative_path def relative_path
changeset.repository.relative_path(path) changeset.repository.relative_path(path)
end end
def before_validation
self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
end
def init_path
self.path ||= ""
end
end end
+56 -177
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments, :description => :long_comments,
:datetime => :committed_on, :datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
acts_as_searchable :columns => 'comments', acts_as_searchable :columns => 'comments',
:include => {:repository => :project}, :include => {:repository => :project},
@@ -41,20 +41,12 @@ class Changeset < ActiveRecord::Base
validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => {:repository => :project},
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
def revision=(r) def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s) write_attribute :revision, (r.nil? ? nil : r.to_s)
end end
# Returns the identifier of this changeset; depending on repository backends def comments=(comment)
def identifier write_attribute(:comments, Changeset.normalize_comments(comment))
if repository.class.respond_to? :changeset_identifier
repository.class.changeset_identifier self
else
revision.to_s
end
end end
def committed_on=(date) def committed_on=(date)
@@ -62,15 +54,6 @@ class Changeset < ActiveRecord::Base
super super
end end
# Returns the readable identifier
def format_identifier
if repository.class.respond_to? :format_changeset_identifier
repository.class.format_changeset_identifier self
else
identifier
end
end
def project def project
repository.project repository.project
end end
@@ -80,56 +63,64 @@ class Changeset < ActiveRecord::Base
end end
def before_create def before_create
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) self.user = repository.find_committer_user(committer)
self.comments = self.class.normalize_comments(
self.comments, repository.repo_log_encoding)
self.user = repository.find_committer_user(self.committer)
end end
def after_create def after_create
scan_comment_for_issue_ids scan_comment_for_issue_ids
end end
require 'pp'
TIMELOG_RE = /
(
((\d+)(h|hours?))((\d+)(m|min)?)?
|
((\d+)(h|hours?|m|min))
|
(\d+):(\d+)
|
(\d+([\.,]\d+)?)h?
)
/x
def scan_comment_for_issue_ids def scan_comment_for_issue_ids
return if comments.blank? return if comments.blank?
# keywords used to reference issues # keywords used to reference issues
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
ref_keywords_any = ref_keywords.delete('*')
# keywords used to fix issues # keywords used to fix issues
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
# status and optional done ratio applied
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
return if kw_regexp.blank?
referenced_issues = [] referenced_issues = []
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| if ref_keywords.delete('*')
action, refs = match[2], match[3] # find any issue ID in the comments
next unless action.present? || ref_keywords_any target_issue_ids = []
comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
if issue
referenced_issues << issue
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
end
end
end end
referenced_issues.uniq! comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
self.issues = referenced_issues unless referenced_issues.empty? action = match[0]
target_issue_ids = match[1].scan(/\d+/)
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
if fix_status && fix_keywords.include?(action.downcase)
# update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
target_issues.each do |issue|
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
issue.save
end
end
referenced_issues += target_issues
end
self.issues = referenced_issues.uniq
end end
def short_comments def short_comments
@@ -140,104 +131,23 @@ class Changeset < ActiveRecord::Base
@long_comments || split_comments.last @long_comments || split_comments.last
end end
def text_tag
if scmid?
"commit:#{scmid}"
else
"r#{revision}"
end
end
# Returns the previous changeset # Returns the previous changeset
def previous def previous
@previous ||= Changeset.find(:first, @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
:conditions => ['id < ? AND repository_id = ?',
self.id, self.repository_id],
:order => 'id DESC')
end end
# Returns the next changeset # Returns the next changeset
def next def next
@next ||= Changeset.find(:first, @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
:conditions => ['id > ? AND repository_id = ?',
self.id, self.repository_id],
:order => 'id ASC')
end end
# Creates a new Change from it's common parameters # Strips and reencodes a commit log before insertion into the database
def create_change(change) def self.normalize_comments(str)
Change.create(:changeset => self, to_utf8(str.to_s.strip)
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end end
private private
# Finds an issue that can be referenced by the commit message
# i.e. an issue that belong to the repository project, a subproject or a parent project
def find_referenced_issue_by_id(id)
return nil if id.blank?
issue = Issue.find_by_id(id.to_i, :include => :project)
if issue
unless issue.project &&
(project == issue.project || project.is_ancestor_of?(issue.project) ||
project.is_descendant_of?(issue.project))
issue = nil
end
end
issue
end
def fix_issue(issue)
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
if status.nil?
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
return issue
end
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.status = status
unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
unless issue.save
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
end
issue
end
def log_time(issue, hours)
time_entry = TimeEntry.new(
:user => user,
:hours => hours,
:issue => issue,
:spent_on => commit_date,
:comments => l(:text_time_logged_by_changeset, :value => text_tag,
:locale => Setting.default_language)
)
time_entry.activity = log_time_activity unless log_time_activity.nil?
unless time_entry.save
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
end
time_entry
end
def log_time_activity
if Setting.commit_logtime_activity_id.to_i > 0
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
end
end
def split_comments def split_comments
comments =~ /\A(.+?)\r?\n(.*)$/m comments =~ /\A(.+?)\r?\n(.*)$/m
@short_comments = $1 || comments @short_comments = $1 || comments
@@ -245,46 +155,15 @@ class Changeset < ActiveRecord::Base
return @short_comments, @long_comments return @short_comments, @long_comments
end end
public def self.to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
# Strips and reencodes a commit log before insertion into the database encoding = Setting.commit_logs_encoding.to_s.strip
def self.normalize_comments(str, encoding) unless encoding.blank? || encoding == 'UTF-8'
Changeset.to_utf8(str.to_s.strip, encoding)
end
def self.to_utf8(str, encoding)
return str if str.nil?
str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
if str.empty?
str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
return str
end
enc = encoding.blank? ? "UTF-8" : encoding
if str.respond_to?(:force_encoding)
if enc.upcase != "UTF-8"
str.force_encoding(enc)
str = str.encode("UTF-8", :invalid => :replace,
:undef => :replace, :replace => '?')
else
str.force_encoding("UTF-8")
if ! str.valid_encoding?
str = str.encode("US-ASCII", :invalid => :replace,
:undef => :replace, :replace => '?').encode("UTF-8")
end
end
else
ic = Iconv.new('UTF-8', enc)
txtar = ""
begin begin
txtar += ic.iconv(str) return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::IllegalSequence rescue Iconv::Failure
txtar += $!.success # do nothing here
str = '?' + $!.failed[1,$!.failed.length]
retry
rescue
txtar += $!.success
end end
str = txtar
end end
str str
end end
-24
View File
@@ -1,24 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CommentObserver < ActiveRecord::Observer
def after_create(comment)
if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
Mailer.deliver_news_comment_added(comment)
end
end
end
+13 -42
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -20,10 +20,20 @@ class CustomField < ActiveRecord::Base
acts_as_list :scope => 'type = \'#{self.class}\'' acts_as_list :scope => 'type = \'#{self.class}\''
serialize :possible_values serialize :possible_values
FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
"text" => { :name => :label_text, :order => 2 },
"int" => { :name => :label_integer, :order => 3 },
"float" => { :name => :label_float, :order => 4 },
"list" => { :name => :label_list, :order => 5 },
"date" => { :name => :label_date, :order => 6 },
"bool" => { :name => :label_boolean, :order => 7 }
}.freeze
validates_presence_of :name, :field_format validates_presence_of :name, :field_format
validates_uniqueness_of :name, :scope => :type validates_uniqueness_of :name, :scope => :type
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
def initialize(attributes = nil) def initialize(attributes = nil)
super super
@@ -42,49 +52,12 @@ class CustomField < ActiveRecord::Base
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
end end
if regexp.present?
begin
Regexp.new(regexp)
rescue
errors.add(:regexp, :invalid)
end
end
# validate default value # validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil) v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false v.custom_field.is_required = false
errors.add(:default_value, :invalid) unless v.valid? errors.add(:default_value, :invalid) unless v.valid?
end end
def possible_values_options(obj=nil)
case field_format
when 'user', 'version'
if obj.respond_to?(:project) && obj.project
case field_format
when 'user'
obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
when 'version'
obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
end
elsif obj.is_a?(Array)
obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
else
[]
end
else
read_attribute :possible_values
end
end
def possible_values(obj=nil)
case field_format
when 'user', 'version'
possible_values_options(obj).collect(&:last)
else
read_attribute :possible_values
end
end
# Makes possible_values accept a multiline string # Makes possible_values accept a multiline string
def possible_values=(arg) def possible_values=(arg)
if arg.is_a?(Array) if arg.is_a?(Array)
@@ -108,8 +81,6 @@ class CustomField < ActiveRecord::Base
casted = value.to_i casted = value.to_i
when 'float' when 'float'
casted = value.to_f casted = value.to_f
when 'user', 'version'
casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
end end
end end
casted casted
+1 -5
View File
@@ -34,10 +34,6 @@ class CustomValue < ActiveRecord::Base
custom_field.editable? custom_field.editable?
end end
def visible?
custom_field.visible?
end
def required? def required?
custom_field.is_required? custom_field.is_required?
end end
@@ -62,7 +58,7 @@ protected
when 'float' when 'float'
begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end
when 'date' when 'date'
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list' when 'list'
errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value) errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
end end
+9 -5
View File
@@ -1,5 +1,5 @@
# RedMine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -29,9 +29,6 @@ class Document < ActiveRecord::Base
validates_presence_of :project, :title, :category validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } }
def visible?(user=User.current) def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_documents, project) !user.nil? && user.allowed_to?(:view_documents, project)
end end
@@ -49,4 +46,11 @@ class Document < ActiveRecord::Base
end end
@updated_on @updated_on
end end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
end end
+2 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+3 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -20,3 +20,4 @@ class DocumentCategoryCustomField < CustomField
:enumeration_doc_categories :enumeration_doc_categories
end end
end end
+2 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+1 -1
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
-1
View File
@@ -32,7 +32,6 @@ class Enumeration < ActiveRecord::Base
named_scope :shared, :conditions => { :project_id => nil } named_scope :shared, :conditions => { :project_id => nil }
named_scope :active, :conditions => { :active => true } named_scope :active, :conditions => { :active => true }
named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
def self.default def self.default
# Creates a fake default scope so Enumeration.default will check # Creates a fake default scope so Enumeration.default will check
-1
View File
@@ -31,7 +31,6 @@ class Group < Principal
def user_added(user) def user_added(user)
members.each do |member| members.each do |member|
next if member.project.nil?
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) 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)
member.member_roles.each do |member_role| member.member_roles.each do |member_role|
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id) user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
+61 -547
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Issue < ActiveRecord::Base class Issue < ActiveRecord::Base
include Redmine::SafeAttributes
belongs_to :project belongs_to :project
belongs_to :tracker belongs_to :tracker
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
@@ -34,7 +32,6 @@ class Issue < ActiveRecord::Base
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_nested_set :scope => 'root_id', :dependent => :destroy
acts_as_attachable :after_remove => :attachment_removed acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable acts_as_customizable
acts_as_watchable acts_as_watchable
@@ -51,71 +48,22 @@ class Issue < ActiveRecord::Base
DONE_RATIO_OPTIONS = %w(issue_field issue_status) DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_reader :current_journal
validates_presence_of :subject, :priority, :project, :tracker, :author, :status validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100 validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true validates_numericality_of :estimated_hours, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => :project, named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Issue.visible_condition(args.shift || User.current, *args) } } :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" before_save :update_done_ratio_from_issue_status
named_scope :with_limit, lambda { |limit| { :limit => limit} } after_save :create_journal
named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :without_version, lambda {
{
:conditions => { :fixed_version_id => nil}
}
}
named_scope :with_query, lambda {|query|
{
:conditions => Query.merge_conditions(query.statement)
}
}
before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_destroy :update_parent_attributes
# Returns a SQL conditions string used to find all issues visible by the specified user
def self.visible_condition(user, options={})
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
case role.issues_visibility
when 'all'
nil
when 'default'
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
when 'own'
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
else
'1=0'
end
end
end
# Returns true if usr or current user is allowed to view the issue # Returns true if usr or current user is allowed to view the issue
def visible?(usr=nil) def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| (usr || User.current).allowed_to?(:view_issues, self.project)
case role.issues_visibility
when 'all'
true
when 'default'
!self.is_private? || self.author == user || self.assigned_to == user
when 'own'
self.author == user || self.assigned_to == user
else
false
end
end
end end
def after_initialize def after_initialize
@@ -128,29 +76,23 @@ class Issue < ActiveRecord::Base
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
def available_custom_fields def available_custom_fields
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
end end
def copy_from(arg) def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} self.custom_values = issue.custom_values.collect {|v| v.clone}
self.status = issue.status self.status = issue.status
self self
end end
# Moves/copies an issue to a new project and tracker # Moves/copies an issue to a new project and tracker
# Returns the moved/copied issue on success, false on failure # Returns the moved/copied issue on success, false on failure
def move_to_project(*args) def move_to(new_project, new_tracker = nil, options = {})
ret = Issue.transaction do
move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
end || false
end
def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
options ||= {} options ||= {}
issue = options[:copy] ? self.class.new.copy_from(self) : self issue = options[:copy] ? self.clone : self
transaction do
if new_project && issue.project_id != new_project.id if new_project && issue.project_id != new_project.id
# delete issue relations # delete issue relations
unless Setting.cross_project_issue_relations? unless Setting.cross_project_issue_relations?
@@ -166,16 +108,11 @@ class Issue < ActiveRecord::Base
issue.fixed_version = nil issue.fixed_version = nil
end end
issue.project = new_project issue.project = new_project
if issue.parent && issue.parent.project_id != issue.project_id
issue.parent_issue_id = nil
end
end end
if new_tracker if new_tracker
issue.tracker = new_tracker issue.tracker = new_tracker
issue.reset_custom_values!
end end
if options[:copy] if options[:copy]
issue.author = User.current
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
issue.status = if options[:attributes] && options[:attributes][:status_id] issue.status = if options[:attributes] && options[:attributes][:status_id]
IssueStatus.find_by_id(options[:attributes][:status_id]) IssueStatus.find_by_id(options[:attributes][:status_id])
@@ -188,32 +125,16 @@ class Issue < ActiveRecord::Base
issue.attributes = options[:attributes] issue.attributes = options[:attributes]
end end
if issue.save if issue.save
if options[:copy] unless options[:copy]
if current_journal && current_journal.notes.present?
issue.init_journal(current_journal.user, current_journal.notes)
issue.current_journal.notify = false
issue.save
end
else
# Manually update project_id on related time entries # Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
issue.children.each do |child|
unless child.move_to_project_without_transaction(new_project)
# Move failed and transaction was rollback'd
return false
end
end
end end
else else
Issue.connection.rollback_db_transaction
return false return false
end end
issue
end end
return issue
def status_id=(sid)
self.status = nil
write_attribute(:status_id, sid)
end end
def priority_id=(pid) def priority_id=(pid)
@@ -223,18 +144,12 @@ class Issue < ActiveRecord::Base
def tracker_id=(tid) def tracker_id=(tid)
self.tracker = nil self.tracker = nil
write_attribute(:tracker_id, tid)
result = write_attribute(:tracker_id, tid) result = write_attribute(:tracker_id, tid)
@custom_field_values = nil @custom_field_values = nil
result result
end end
def description=(arg)
if arg.is_a?(String)
arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
end
write_attribute(:description, arg)
end
# Overrides attributes= so that tracker_id gets assigned first # Overrides attributes= so that tracker_id gets assigned first
def attributes_with_tracker_first=(new_attributes, *args) def attributes_with_tracker_first=(new_attributes, *args)
return if new_attributes.nil? return if new_attributes.nil?
@@ -244,83 +159,14 @@ class Issue < ActiveRecord::Base
end end
send :attributes_without_tracker_first=, new_attributes, *args send :attributes_without_tracker_first=, new_attributes, *args
end end
# Do not redefine alias chain on reload (see #4838) alias_method_chain :attributes=, :tracker_first
alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
def estimated_hours=(h) def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end end
safe_attributes 'tracker_id',
'status_id',
'parent_issue_id',
'category_id',
'assigned_to_id',
'priority_id',
'fixed_version_id',
'subject',
'description',
'start_date',
'due_date',
'done_ratio',
'estimated_hours',
'custom_field_values',
'custom_fields',
'lock_version',
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
safe_attributes 'status_id',
'assigned_to_id',
'fixed_version_id',
'done_ratio',
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
safe_attributes 'is_private',
:if => lambda {|issue, user|
user.allowed_to?(:set_issues_private, issue.project) ||
(issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
}
# Safely sets attributes
# Should be called from controllers instead of #attributes=
# attr_accessible is too rough because we still want things like
# Issue.new(:project => foo) to work
# TODO: move workflow/permission checks from controllers to here
def safe_attributes=(attrs, user=User.current)
return unless attrs.is_a?(Hash)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
attrs = delete_unsafe_attributes(attrs, user)
return if attrs.empty?
# Tracker must be set before since new_statuses_allowed_to depends on it.
if t = attrs.delete('tracker_id')
self.tracker_id = t
end
if attrs['status_id']
unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
attrs.delete('status_id')
end
end
unless leaf?
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
end
if attrs.has_key?('parent_issue_id')
if !user.allowed_to?(:manage_subtasks, project)
attrs.delete('parent_issue_id')
elsif !attrs['parent_issue_id'].blank?
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
end
end
self.attributes = attrs
end
def done_ratio def done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
status.default_done_ratio status.default_done_ratio
else else
read_attribute(:done_ratio) read_attribute(:done_ratio)
@@ -362,32 +208,44 @@ class Issue < ActiveRecord::Base
errors.add :tracker_id, :inclusion errors.add :tracker_id, :inclusion
end end
end end
end
# Checks parent issue assignment def before_create
if @parent_issue # default assignment based on category
if @parent_issue.project_id != project_id if assigned_to.nil? && category && category.assigned_to
errors.add :parent_issue_id, :not_same_project self.assigned_to = category.assigned_to
elsif !new_record?
# moving an existing issue
if @parent_issue.root_id != root_id
# we can always move to another tree
elsif move_possible?(@parent_issue)
# move accepted inside tree
else
errors.add :parent_issue_id, :not_a_valid_parent
end
end
end end
end end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later # even if the user turns off the setting later
def update_done_ratio_from_issue_status def update_done_ratio_from_issue_status
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
self.done_ratio = status.default_done_ratio self.done_ratio = status.default_done_ratio
end end
end end
def after_save
# Reload is needed in order to get the right status
reload
# Update start/due dates of following issues
relations_from.each(&:set_issue_to_dates)
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
duplicate.init_journal(@current_journal.user, @current_journal.notes)
duplicate.update_attribute :status, self.status
end
end
end
def init_journal(user, notes = "") def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone @issue_before_change = self.clone
@@ -416,41 +274,14 @@ class Issue < ActiveRecord::Base
false false
end end
# Return true if the issue is being closed
def closing?
if !new_record? && status_id_changed?
status_was = IssueStatus.find_by_id(status_id_was)
status_new = IssueStatus.find_by_id(status_id)
if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
return true
end
end
false
end
# Returns true if the issue is overdue # Returns true if the issue is overdue
def overdue? def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed? !due_date.nil? && (due_date < Date.today) && !status.is_closed?
end end
# Is the amount of work done less than it should for the due date
def behind_schedule?
return false if start_date.nil? || due_date.nil?
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
return done_date <= Date.today
end
# Does this issue have children?
def children?
!leaf?
end
# Users the issue can be assigned to # Users the issue can be assigned to
def assignable_users def assignable_users
users = project.assignable_users project.assignable_users
users << author if author
users << assigned_to if assigned_to
users.uniq.sort
end end
# Versions that the issue can be assigned to # Versions that the issue can be assigned to
@@ -464,15 +295,9 @@ class Issue < ActiveRecord::Base
end end
# Returns an array of status that user is able to apply # Returns an array of status that user is able to apply
def new_statuses_allowed_to(user, include_default=false) def new_statuses_allowed_to(user)
statuses = status.find_new_statuses_allowed_to( statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
user.roles_for_project(project),
tracker,
author == user,
assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
)
statuses << status unless statuses.empty? statuses << status unless statuses.empty?
statuses << IssueStatus.default if include_default
statuses = statuses.uniq.sort statuses = statuses.uniq.sort
blocked? ? statuses.reject {|s| s.is_closed?} : statuses blocked? ? statuses.reject {|s| s.is_closed?} : statuses
end end
@@ -480,37 +305,33 @@ class Issue < ActiveRecord::Base
# Returns the mail adresses of users that should be notified # Returns the mail adresses of users that should be notified
def recipients def recipients
notified = project.notified_users notified = project.notified_users
# Author and assignee are always notified unless they have been # Author and assignee are always notified unless they have been locked
# locked or don't want to be notified notified << author if author && author.active?
notified << author if author && author.active? && author.notify_about?(self) notified << assigned_to if assigned_to && assigned_to.active?
notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
notified.uniq! notified.uniq!
# Remove users that can not view the issue # Remove users that can not view the issue
notified.reject! {|user| !visible?(user)} notified.reject! {|user| !visible?(user)}
notified.collect(&:mail) notified.collect(&:mail)
end end
# Returns the total number of hours spent on this issue and its descendants # Returns the total number of hours spent on this issue.
# #
# Example: # Example:
# spent_hours => 0.0 # spent_hours => 0
# spent_hours => 50.2 # spent_hours => 50
def spent_hours def spent_hours
@spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 @spent_hours ||= time_entries.sum(:hours) || 0
end end
def relations def relations
(relations_from + relations_to).sort (relations_from + relations_to).sort
end end
def all_dependent_issues(except=[]) def all_dependent_issues
except << self
dependencies = [] dependencies = []
relations_from.each do |relation| relations_from.each do |relation|
if relation.issue_to && !except.include?(relation.issue_to)
dependencies << relation.issue_to dependencies << relation.issue_to
dependencies += relation.issue_to.all_dependent_issues(except) dependencies += relation.issue_to.all_dependent_issues
end
end end
dependencies dependencies
end end
@@ -536,34 +357,7 @@ class Issue < ActiveRecord::Base
end end
def soonest_start def soonest_start
@soonest_start ||= ( @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
relations_to.collect{|relation| relation.successor_soonest_start} +
ancestors.collect(&:soonest_start)
).compact.max
end
def reschedule_after(date)
return if date.nil?
if leaf?
if start_date.nil? || start_date < date
self.start_date, self.due_date = date, date + duration
save
end
else
leaves.each do |leaf|
leaf.reschedule_after(date)
end
end
end
def <=>(issue)
if issue.nil?
-1
elsif root_id != issue.root_id
(root_id || 0) <=> (issue.root_id || 0)
else
(lft || 0) <=> (issue.lft || 0)
end
end end
def to_s def to_s
@@ -575,50 +369,11 @@ class Issue < ActiveRecord::Base
s = "issue status-#{status.position} priority-#{priority.position}" s = "issue status-#{status.position} priority-#{priority.position}"
s << ' closed' if closed? s << ' closed' if closed?
s << ' overdue' if overdue? s << ' overdue' if overdue?
s << ' child' if child?
s << ' parent' unless leaf?
s << ' private' if is_private?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s s
end end
# Saves an issue, time_entry, attachments, and a journal from the parameters
# Returns false if save fails
def save_issue_with_child_records(params, existing_time_entry=nil)
Issue.transaction do
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
@time_entry = existing_time_entry || TimeEntry.new
@time_entry.project = project
@time_entry.issue = self
@time_entry.user = User.current
@time_entry.spent_on = Date.today
@time_entry.attributes = params[:time_entry]
self.time_entries << @time_entry
end
if valid?
attachments = Attachment.attach_files(self, params[:attachments])
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
begin
if save
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
else
raise ActiveRecord::Rollback
end
rescue ActiveRecord::StaleObjectError
attachments[:files].each(&:destroy)
errors.add_to_base l(:notice_locking_conflict)
raise ActiveRecord::Rollback
end
end
end
end
# Unassigns issues from +version+ if it's no longer shared with issue's project # Unassigns issues from +version+ if it's no longer shared with issue's project
def self.update_versions_from_sharing_change(version) def self.update_versions_from_sharing_change(version)
# Update issues assigned to the version # Update issues assigned to the version
@@ -633,183 +388,8 @@ class Issue < ActiveRecord::Base
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
end end
def parent_issue_id=(arg)
parent_issue_id = arg.blank? ? nil : arg.to_i
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
@parent_issue.id
else
@parent_issue = nil
nil
end
end
def parent_issue_id
if instance_variable_defined? :@parent_issue
@parent_issue.nil? ? nil : @parent_issue.id
else
parent_id
end
end
# Extracted from the ReportsController.
def self.by_tracker(project)
count_and_group_by(:project => project,
:field => 'tracker_id',
:joins => Tracker.table_name)
end
def self.by_version(project)
count_and_group_by(:project => project,
:field => 'fixed_version_id',
:joins => Version.table_name)
end
def self.by_priority(project)
count_and_group_by(:project => project,
:field => 'priority_id',
:joins => IssuePriority.table_name)
end
def self.by_category(project)
count_and_group_by(:project => project,
:field => 'category_id',
:joins => IssueCategory.table_name)
end
def self.by_assigned_to(project)
count_and_group_by(:project => project,
:field => 'assigned_to_id',
:joins => User.table_name)
end
def self.by_author(project)
count_and_group_by(:project => project,
:field => 'author_id',
:joins => User.table_name)
end
def self.by_subproject(project)
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
#{Issue.table_name}.project_id as project_id,
count(#{Issue.table_name}.id) as total
from
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
where
#{Issue.table_name}.status_id=s.id
and #{Issue.table_name}.project_id = #{Project.table_name}.id
and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
and #{Issue.table_name}.project_id <> #{project.id}
group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
end
# End ReportsController extraction
# Returns an array of projects that current user can move issues to
def self.allowed_target_projects_on_move
projects = []
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
projects = Project.visible.all
elsif User.current.logged?
if Role.non_member.allowed_to?(:move_issues)
projects = Project.visible.all
else
User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
end
end
projects
end
private private
def update_nested_set_attributes
if root_id.nil?
# issue was just created
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
set_default_left_and_right
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
if @parent_issue
move_to_child_of(@parent_issue)
end
reload
elsif parent_issue_id != parent_id
former_parent_id = parent_id
# moving an existing issue
if @parent_issue && @parent_issue.root_id == root_id
# inside the same tree
move_to_child_of(@parent_issue)
else
# to another tree
unless root?
move_to_right_of(root)
reload
end
old_root_id = root_id
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
target_maxright = nested_set_scope.maximum(right_column_name) || 0
offset = target_maxright + 1 - lft
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
self[left_column_name] = lft + offset
self[right_column_name] = rgt + offset
if @parent_issue
move_to_child_of(@parent_issue)
end
end
reload
# delete invalid relations of all descendants
self_and_descendants.each do |issue|
issue.relations.each do |relation|
relation.destroy unless relation.valid?
end
end
# update former parent
recalculate_attributes_for(former_parent_id) if former_parent_id
end
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
end
def update_parent_attributes
recalculate_attributes_for(parent_id) if parent_id
end
def recalculate_attributes_for(issue_id)
if issue_id && p = Issue.find_by_id(issue_id)
# priority = highest priority of children
if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
p.priority = IssuePriority.find_by_position(priority_position)
end
# start/due dates = lowest/highest dates of children
p.start_date = p.children.minimum(:start_date)
p.due_date = p.children.maximum(:due_date)
if p.start_date && p.due_date && p.due_date < p.start_date
p.start_date, p.due_date = p.due_date, p.start_date
end
# done ratio = weighted average ratio of leaves
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
leaves_count = p.leaves.count
if leaves_count > 0
average = p.leaves.average(:estimated_hours).to_f
if average == 0
average = 1
end
done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
progress = done / (average * leaves_count)
p.done_ratio = progress.round
end
end
# estimate = sum of leaves estimates
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
p.estimated_hours = nil if p.estimated_hours == 0.0
# ancestors will be recursively updated
p.save(false)
end
end
# Update issues so their versions are not pointing to a # Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project # fixed_version that is not shared with the issue's project
def self.update_versions(conditions=nil) def self.update_versions(conditions=nil)
@@ -839,52 +419,16 @@ class Issue < ActiveRecord::Base
journal.save journal.save
end end
# Default assignment based on category
def default_assign
if assigned_to.nil? && category && category.assigned_to
self.assigned_to = category.assigned_to
end
end
# Updates start/due dates of following issues
def reschedule_following_issues
if start_date_changed? || due_date_changed?
relations_from.each do |relation|
relation.set_issue_to_dates
end
end
end
# Closes duplicates if the issue is being closed
def close_duplicates
if closing?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
if @current_journal
duplicate.init_journal(@current_journal.user, @current_journal.notes)
end
duplicate.update_attribute :status, self.status
end
end
end
# Saves the changes in a Journal # Saves the changes in a Journal
# Called after_save # Called after_save
def create_journal def create_journal
if @current_journal if @current_journal
# attributes changes # attributes changes
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
before = @issue_before_change.send(c)
after = send(c)
next if before == after || (before.blank? && after.blank?)
@current_journal.details << JournalDetail.new(:property => 'attr', @current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c, :prop_key => c,
:old_value => @issue_before_change.send(c), :old_value => @issue_before_change.send(c),
:value => send(c)) :value => send(c)) unless send(c)==@issue_before_change.send(c)
} }
# custom fields changes # custom fields changes
custom_values.each {|c| custom_values.each {|c|
@@ -896,36 +440,6 @@ class Issue < ActiveRecord::Base
:value => c.value) :value => c.value)
} }
@current_journal.save @current_journal.save
# reset current journal
init_journal @current_journal.user, @current_journal.notes
end end
end end
# Query generator for selecting groups of issue counts for a project
# based on specific criteria
#
# Options
# * project - Project to search in.
# * field - String. Issue field to key off of in the grouping.
# * joins - String. The table name to join against.
def self.count_and_group_by(options)
project = options.delete(:project)
select_field = options.delete(:field)
joins = options.delete(:joins)
where = "#{Issue.table_name}.#{select_field}=j.id"
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
j.id as #{select_field},
count(#{Issue.table_name}.id) as total
from
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
where
#{Issue.table_name}.status_id=s.id
and #{where}
and #{Issue.table_name}.project_id=#{Project.table_name}.id
and #{visible_condition(User.current, :project => project)}
group by s.id, s.is_closed, j.id")
end
end end
+2 -4
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -24,8 +24,6 @@ class IssueCategory < ActiveRecord::Base
validates_uniqueness_of :name, :scope => [:project_id] validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
alias :destroy_without_reassign :destroy alias :destroy_without_reassign :destroy
# Destroy the category # Destroy the category
+11 -33
View File
@@ -21,19 +21,15 @@ class IssueRelation < ActiveRecord::Base
TYPE_RELATES = "relates" TYPE_RELATES = "relates"
TYPE_DUPLICATES = "duplicates" TYPE_DUPLICATES = "duplicates"
TYPE_DUPLICATED = "duplicated"
TYPE_BLOCKS = "blocks" TYPE_BLOCKS = "blocks"
TYPE_BLOCKED = "blocked"
TYPE_PRECEDES = "precedes" TYPE_PRECEDES = "precedes"
TYPE_FOLLOWS = "follows" TYPE_FOLLOWS = "follows"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES }, TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED }, TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED }, TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 5 }
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
}.freeze }.freeze
validates_presence_of :issue_from, :issue_to, :relation_type validates_presence_of :issue_from, :issue_to, :relation_type
@@ -47,31 +43,14 @@ class IssueRelation < ActiveRecord::Base
if issue_from && issue_to if issue_from && issue_to
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
#detect circular dependencies depending wether the relation should be reversed
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
errors.add_to_base :circular_dependency if issue_from.all_dependent_issues.include? issue_to
else
errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
end end
errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
end
end end
def other_issue(issue) def other_issue(issue)
(self.issue_from_id == issue.id) ? issue_to : issue_from (self.issue_from_id == issue.id) ? issue_to : issue_from
end end
# Returns the relation type for +issue+
def relation_type_for(issue)
if TYPES[relation_type]
if self.issue_from_id == issue.id
relation_type
else
TYPES[relation_type][:sym]
end
end
end
def label_for(issue) def label_for(issue)
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
end end
@@ -89,16 +68,16 @@ class IssueRelation < ActiveRecord::Base
def set_issue_to_dates def set_issue_to_dates
soonest_start = self.successor_soonest_start soonest_start = self.successor_soonest_start
if soonest_start && issue_to if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
issue_to.reschedule_after(soonest_start) issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
issue_to.save
end end
end end
def successor_soonest_start def successor_soonest_start
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date) return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
(issue_from.due_date || issue_from.start_date) + 1 + delay (issue_from.due_date || issue_from.start_date) + 1 + delay
end end
end
def <=>(relation) def <=>(relation)
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
@@ -106,13 +85,12 @@ class IssueRelation < ActiveRecord::Base
private private
# Reverses the relation if needed so that it gets stored in the proper way
def reverse_if_needed def reverse_if_needed
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] if (TYPE_FOLLOWS == relation_type)
issue_tmp = issue_to issue_tmp = issue_to
self.issue_to = issue_from self.issue_to = issue_from
self.issue_from = issue_tmp self.issue_from = issue_tmp
self.relation_type = TYPES[relation_type][:reverse] self.relation_type = TYPE_PRECEDES
end end
end end
end end
+16 -27
View File
@@ -17,18 +17,15 @@
class IssueStatus < ActiveRecord::Base class IssueStatus < ActiveRecord::Base
before_destroy :check_integrity before_destroy :check_integrity
has_many :workflows, :foreign_key => "old_status_id" has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
acts_as_list acts_as_list
before_destroy :delete_workflows
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name validates_uniqueness_of :name
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
def after_save def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
end end
@@ -52,15 +49,10 @@ class IssueStatus < ActiveRecord::Base
# Returns an array of all statuses the given role can switch to # Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time # Uses association cache when called more than one time
def new_statuses_allowed_to(roles, tracker, author=false, assignee=false) def new_statuses_allowed_to(roles, tracker)
if roles && tracker if roles && tracker
role_ids = roles.collect(&:id) role_ids = roles.collect(&:id)
transitions = workflows.select do |w| new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
role_ids.include?(w.role_id) &&
w.tracker_id == tracker.id &&
((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
end
transitions.collect{|w| w.new_status}.compact.sort
else else
[] []
end end
@@ -68,23 +60,25 @@ class IssueStatus < ActiveRecord::Base
# Same thing as above but uses a database query # Same thing as above but uses a database query
# More efficient than the previous method if called just once # More efficient than the previous method if called just once
def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false) def find_new_statuses_allowed_to(roles, tracker)
if roles.present? && tracker if roles && tracker
conditions = "(author = :false AND assignee = :false)"
conditions << " OR author = :true" if author
conditions << " OR assignee = :true" if assignee
workflows.find(:all, workflows.find(:all,
:include => :new_status, :include => :new_status,
:conditions => ["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})", :conditions => { :role_id => roles.collect(&:id),
{:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false} :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
]
).collect{|w| w.new_status}.compact.sort
else else
[] []
end end
end end
def new_status_allowed_to?(status, roles, tracker)
if status && roles && tracker
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
else
false
end
end
def <=>(status) def <=>(status)
position <=> status.position position <=> status.position
end end
@@ -95,9 +89,4 @@ private
def check_integrity def check_integrity
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
end end
# Deletes associated workflows
def delete_workflows
Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
end
end end
+3 -23
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -32,16 +32,12 @@ class Journal < ActiveRecord::Base
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
acts_as_activity_provider :type => 'issues', acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id, :author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user], :find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
named_scope :visible, lambda {|*args| {
:include => {:issue => :project},
:conditions => Issue.visible_condition(args.shift || User.current, *args)
}}
def save(*args) def save(*args)
# Do not save an empty journal # Do not save an empty journal
(details.empty? && notes.blank?) ? false : super (details.empty? && notes.blank?) ? false : super
@@ -69,20 +65,4 @@ class Journal < ActiveRecord::Base
def attachments def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil journalized.respond_to?(:attachments) ? journalized.attachments : nil
end end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.blank?
s
end
def notify?
@notify != false
end
def notify=(arg)
@notify = arg
end
end end
+5 -18
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,22 +17,9 @@
class JournalDetail < ActiveRecord::Base class JournalDetail < ActiveRecord::Base
belongs_to :journal belongs_to :journal
before_save :normalize_values
private def before_save
self.value = value[0..254] if value && value.is_a?(String)
def normalize_values self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
self.value = normalize(value)
self.old_value = normalize(old_value)
end
def normalize(v)
if v == true
"1"
elsif v == false
"0"
else
v
end
end end
end end
+3 -10
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,13 +17,6 @@
class JournalObserver < ActiveRecord::Observer class JournalObserver < ActiveRecord::Observer
def after_create(journal) def after_create(journal)
if journal.notify? && Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
(Setting.notified_events.include?('issue_updated') ||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
)
Mailer.deliver_issue_edit(journal)
end
end end
end end
+44 -94
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,7 +17,6 @@
class MailHandler < ActionMailer::Base class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
include Redmine::I18n
class UnauthorizedAction < StandardError; end class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end class MissingInformation < StandardError; end
@@ -50,7 +49,7 @@ class MailHandler < ActionMailer::Base
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
return false return false
end end
@user = User.find_by_mail(sender_email) if sender_email.present? @user = User.find_by_mail(sender_email)
if @user && !@user.active? if @user && !@user.active?
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
return false return false
@@ -100,7 +99,7 @@ class MailHandler < ActionMailer::Base
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE) elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
receive_message_reply(m[1].to_i) receive_message_reply(m[1].to_i)
else else
dispatch_to_default receive_issue
end end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user # TODO: send a email to the user
@@ -114,27 +113,36 @@ class MailHandler < ActionMailer::Base
false false
end end
def dispatch_to_default
receive_issue
end
# Creates a new issue # Creates a new issue
def receive_issue def receive_issue
project = target_project project = target_project
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
# check permission # check permission
unless @@handler_options[:no_permission_check] unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end end
issue = Issue.new(:author => user, :project => project) issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
issue.safe_attributes = issue_attributes_from_keywords(issue) # check workflow
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} if status && issue.new_statuses_allowed_to(user).include?(status)
issue.subject = email.subject.to_s.chomp[0,255] issue.status = status
end
issue.subject = email.subject.chomp
if issue.subject.blank? if issue.subject.blank?
issue.subject = '(no subject)' issue.subject = '(no subject)'
end end
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
h[c.id] = value
end
h
end
issue.description = cleaned_up_text_body issue.description = cleaned_up_text_body
# add To and Cc as watchers before saving so the watchers can reply to Redmine # add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue) add_watchers(issue)
issue.save! issue.save!
@@ -143,23 +151,34 @@ class MailHandler < ActionMailer::Base
issue issue
end end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Adds a note to an existing issue # Adds a note to an existing issue
def receive_issue_reply(issue_id) def receive_issue_reply(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.find_by_id(issue_id) issue = Issue.find_by_id(issue_id)
return unless issue return unless issue
# check permission # check permission
unless @@handler_options[:no_permission_check] unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
end end
# ignore CLI-supplied defaults for new issues # add the note
@@handler_options[:issue].clear journal = issue.init_journal(user, cleaned_up_text_body)
journal = issue.init_journal(user)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
journal.notes = cleaned_up_text_body
add_attachments(issue) add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.save! issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal journal
@@ -198,7 +217,7 @@ class MailHandler < ActionMailer::Base
end end
def add_attachments(obj) def add_attachments(obj)
if email.attachments && email.attachments.any? if email.has_attachments?
email.attachments.each do |attachment| email.attachments.each do |attachment|
Attachment.create(:container => obj, Attachment.create(:container => obj,
:file => attachment, :file => attachment,
@@ -226,8 +245,8 @@ class MailHandler < ActionMailer::Base
@keywords[attr] @keywords[attr]
else else
@keywords[attr] = begin @keywords[attr] = begin
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format])) if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
v $1.strip
elsif !@@handler_options[:issue][attr].blank? elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr] @@handler_options[:issue][attr]
end end
@@ -235,65 +254,6 @@ class MailHandler < ActionMailer::Base
end end
end end
# Destructively extracts the value for +attr+ in +text+
# Returns nil if no matching keyword found
def extract_keyword!(text, attr, format=nil)
keys = [attr.to_s.humanize]
if attr.is_a?(Symbol)
keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
end
keys.reject! {|k| k.blank?}
keys.collect! {|k| Regexp.escape(k)}
format ||= '.+'
text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
$2 && $2.strip
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Returns a Hash of issue attributes extracted from keywords in the email body
def issue_attributes_from_keywords(issue)
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
attrs = {
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
'assigned_to_id' => assigned_to.try(:id),
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
}.delete_if {|k, v| v.blank? }
if issue.new_record? && attrs['tracker_id'].nil?
attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
end
attrs
end
# Returns a Hash of issue custom field values extracted from keywords in the email body
def custom_field_values_from_keywords(customized)
customized.custom_field_values.inject({}) do |h, v|
if value = get_keyword(v.custom_field.name, :override => true)
h[v.custom_field.id.to_s] = value
end
h
end
end
# Returns the text/plain part of the email # Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed # If not found (eg. HTML-only email), returns the body with tags removed
def plain_text_body def plain_text_body
@@ -348,19 +308,9 @@ class MailHandler < ActionMailer::Base
def cleanup_body(body) def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty? unless delimiters.empty?
regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
body = body.gsub(regex, '') body = body.gsub(regex, '')
end end
body.strip body.strip
end end
def find_user_from_keyword(keyword)
user ||= User.find_by_mail(keyword)
user ||= User.find_by_login(keyword)
if user.nil? && keyword.match(/ /)
firstname, lastname = *(keyword.split) # "First Last Throwaway"
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
end
user
end
end end
+17 -72
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -15,8 +15,6 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'ar_condition'
class Mailer < ActionMailer::Base class Mailer < ActionMailer::Base
layout 'mailer' layout 'mailer'
helper :application helper :application
@@ -82,10 +80,10 @@ class Mailer < ActionMailer::Base
def reminder(user, issues, days) def reminder(user, issues, days)
set_language_if_valid user.language set_language_if_valid user.language
recipients user.mail recipients user.mail
subject l(:mail_subject_reminder, :count => issues.size, :days => days) subject l(:mail_subject_reminder, issues.size)
body :issues => issues, body :issues => issues,
:days => days, :days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort => 'due_date:asc') :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
render_multipart('reminder', body) render_multipart('reminder', body)
end end
@@ -114,13 +112,13 @@ class Mailer < ActionMailer::Base
added_to_url = '' added_to_url = ''
case container.class.name case container.class.name
when 'Project' when 'Project'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container) added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}" added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Version' when 'Version'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project) added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}" added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Document' when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}" added_to = "#{l(:label_document)}: #{container.title}"
@@ -149,24 +147,6 @@ class Mailer < ActionMailer::Base
render_multipart('news_added', body) render_multipart('news_added', body)
end end
# Builds a tmail object used to email recipients of a news' project when a news comment is added.
#
# Example:
# news_comment_added(comment) => tmail object
# Mailer.news_comment_added(comment) => sends an email to the news' project recipients
def news_comment_added(comment)
news = comment.commented
redmine_headers 'Project' => news.project.identifier
message_id comment
recipients news.recipients
cc news.watcher_recipients
subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:comment => comment,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
render_multipart('news_comment_added', body)
end
# Builds a tmail object used to email the recipients of the specified message that was posted. # Builds a tmail object used to email the recipients of the specified message that was posted.
# #
# Example: # Example:
@@ -181,7 +161,7 @@ class Mailer < ActionMailer::Base
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients) cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message, body :message => message,
:message_url => url_for(message.event_url) :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
render_multipart('message_posted', body) render_multipart('message_posted', body)
end end
@@ -196,9 +176,9 @@ class Mailer < ActionMailer::Base
message_id wiki_content message_id wiki_content
recipients wiki_content.recipients recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients) cc(wiki_content.page.wiki.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}" subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content, body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title) :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
render_multipart('wiki_content_added', body) render_multipart('wiki_content_added', body)
end end
@@ -213,10 +193,10 @@ class Mailer < ActionMailer::Base
message_id wiki_content message_id wiki_content
recipients wiki_content.recipients recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients) cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}" subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content, body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title), :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
:wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :project_id => wiki_content.project, :id => wiki_content.page.title, :version => wiki_content.version) :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
render_multipart('wiki_content_updated', body) render_multipart('wiki_content_updated', body)
end end
@@ -304,21 +284,7 @@ class Mailer < ActionMailer::Base
if @references_objects if @references_objects
mail.references = @references_objects.collect {|o| self.class.message_id_for(o)} mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
end end
super(mail)
# Log errors when raise_delivery_errors is set to false, Rails does not
raise_errors = self.class.raise_delivery_errors
self.class.raise_delivery_errors = true
begin
return super(mail)
rescue Exception => e
if raise_errors
raise e
elsif mylogger
mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
end
ensure
self.class.raise_delivery_errors = raise_errors
end
end end
# Sends reminders to issue assignees # Sends reminders to issue assignees
@@ -326,16 +292,13 @@ class Mailer < ActionMailer::Base
# * :days => how many days in the future to remind about (defaults to 7) # * :days => how many days in the future to remind about (defaults to 7)
# * :tracker => id of tracker for filtering issues (defaults to all trackers) # * :tracker => id of tracker for filtering issues (defaults to all trackers)
# * :project => id or identifier of project to process (defaults to all projects) # * :project => id or identifier of project to process (defaults to all projects)
# * :users => array of user ids who should be reminded
def self.reminders(options={}) def self.reminders(options={})
days = options[:days] || 7 days = options[:days] || 7
project = options[:project] ? Project.find(options[:project]) : nil project = options[:project] ? Project.find(options[:project]) : nil
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
user_ids = options[:users]
s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
s << ["#{Issue.table_name}.assigned_to_id IN (?)", user_ids] if user_ids.present?
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
s << "#{Issue.table_name}.project_id = #{project.id}" if project s << "#{Issue.table_name}.project_id = #{project.id}" if project
s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
@@ -344,19 +307,10 @@ class Mailer < ActionMailer::Base
:conditions => s.conditions :conditions => s.conditions
).group_by(&:assigned_to) ).group_by(&:assigned_to)
issues_by_assignee.each do |assignee, issues| issues_by_assignee.each do |assignee, issues|
deliver_reminder(assignee, issues, days) if assignee && assignee.active? deliver_reminder(assignee, issues, days) unless assignee.nil?
end end
end end
# Activates/desactivates email deliveries during +block+
def self.with_deliveries(enabled = true, &block)
was_enabled = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = !!enabled
yield
ensure
ActionMailer::Base.perform_deliveries = was_enabled
end
private private
def initialize_defaults(method_name) def initialize_defaults(method_name)
super super
@@ -386,14 +340,9 @@ class Mailer < ActionMailer::Base
recipients.delete(@author.mail) if recipients recipients.delete(@author.mail) if recipients
cc.delete(@author.mail) if cc cc.delete(@author.mail) if cc
end end
notified_users = [recipients, cc].flatten.compact.uniq
# Rails would log recipients only, not cc and bcc
mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
# Blind carbon copy recipients # Blind carbon copy recipients
if Setting.bcc_recipients? if Setting.bcc_recipients?
bcc(notified_users) bcc([recipients, cc].flatten.compact.uniq)
recipients [] recipients []
cc [] cc []
end end
@@ -444,10 +393,6 @@ class Mailer < ActionMailer::Base
@references_objects ||= [] @references_objects ||= []
@references_objects << object @references_objects << object
end end
def mylogger
RAILS_DEFAULT_LOGGER
end
end end
# Patch TMail so that message_id is not overwritten # Patch TMail so that message_id is not overwritten
+1 -8
View File
@@ -72,17 +72,10 @@ class Member < ActiveRecord::Base
end end
end end
# Find or initilize a Member with an id, attributes, and for a Principal
def self.edit_membership(id, new_attributes, principal=nil)
@membership = id.present? ? Member.find(id) : Member.new(:principal => principal)
@membership.attributes = new_attributes
@membership
end
protected protected
def validate def validate
errors.add_on_empty :role if member_roles.empty? && roles.empty? errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
end end
private private
+11 -7
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -24,13 +24,13 @@ class Message < ActiveRecord::Base
acts_as_searchable :columns => ['subject', 'content'], acts_as_searchable :columns => ['subject', 'content'],
:include => {:board => :project}, :include => {:board => :project},
:project_key => "#{Board.table_name}.project_id", :project_key => 'project_id',
:date_column => "#{table_name}.created_on" :date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content, :description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
{:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})} {:id => o.parent_id, :anchor => "message-#{o.id}"})}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
:author_key => :author_id :author_key => :author_id
@@ -42,9 +42,6 @@ class Message < ActiveRecord::Base
after_create :add_author_as_watcher after_create :add_author_as_watcher
named_scope :visible, lambda {|*args| { :include => {:board => :project},
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
def visible?(user=User.current) def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_messages, project) !user.nil? && user.allowed_to?(:view_messages, project)
end end
@@ -93,6 +90,13 @@ class Message < ActiveRecord::Base
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
end end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
private private
def add_author_as_watcher def add_author_as_watcher
+2 -2
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
Loaded 100 of 1099 files, more files were not shown because too many files have changed in this diff. Show more