Compare commits

...
55 Commits
Author SHA1 Message Date
Jean-Philippe Lang a84920ca70 Merged r3865 and r3866 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3868 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-25 09:34:13 +00:00
Eric Davis 21d2de48b7 Merged r3836 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3838 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-08 03:54:53 +00:00
Eric Davis a19a37b600 Merged r3835 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3837 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-08 03:54:48 +00:00
Eric Davis 29e7b8d9ea Merged r3832 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3834 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-08 03:48:40 +00:00
Eric Davis f3b9c0027f Merged r3831 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3833 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-08 03:48:35 +00:00
Eric Davis a146c2d03f Merged r3815 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3817 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-28 02:48:12 +00:00
Eric Davis a62ff0841f Merged r3814 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3816 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-28 02:48:06 +00:00
Eric Davis d7ec02691f Merged r3811 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3812 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-24 02:39:21 +00:00
Eric Davis e7790bb6b5 Fix nil method error when no issue params are submitted. #5123
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3796 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-20 03:24:38 +00:00
Eric Davis 34f73b005b Merged r3785 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3795 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 23:03:12 +00:00
Eric Davis ba42e1e2ff Merged r3786 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3794 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 23:03:07 +00:00
Eric Davis 14074da8b1 Merged r3784 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3793 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 23:03:01 +00:00
Eric Davis 44851d079f Merged r3787 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3792 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 23:02:55 +00:00
Eric Davis e6d053c3fd Merged r3783 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3791 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 23:02:50 +00:00
Eric Davis f17924c60d Merged r3775 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3781 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 18:47:18 +00:00
Eric Davis 90e23e4d5d Merged r3777 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3780 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 18:47:13 +00:00
Eric Davis 88a2792778 Merged r3776 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3779 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 18:47:08 +00:00
Eric Davis 4c881d54a7 Merged r3659 from trunk
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3778 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-06-19 18:47:03 +00:00
Jean-Philippe Lang a6d45cc68f Merged r3740 and r3741 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3742 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-05-22 08:55:56 +00:00
Jean-Philippe Lang f58f7e0e41 Merged r3727 from trunk for 0.9.4 release.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3728 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-05-01 12:38:44 +00:00
Jean-Philippe Lang 473366f887 Merged r3725 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3726 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-05-01 12:33:27 +00:00
Jean-Philippe Lang f6d2a4c29f Backported r3683 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3717 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 17:32:42 +00:00
Jean-Philippe Lang b26d0fe041 Merged r3705 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3715 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 17:20:59 +00:00
Jean-Philippe Lang 98f3e98d82 Merged r3702 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3714 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 17:19:40 +00:00
Jean-Philippe Lang 7b27280c9b Backported r3692 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3713 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 14:22:52 +00:00
Jean-Philippe Lang d287436627 Merged r3678 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3712 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 14:06:45 +00:00
Jean-Philippe Lang 666d3d6472 Merged r3682 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3711 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-30 14:05:37 +00:00
Eric Davis 36063f16ee Merged r3698 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3699 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-29 23:30:21 +00:00
Jean-Philippe Lang 4a295e723e Merged r3668 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3697 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-29 22:05:49 +00:00
Jean-Philippe Lang 6e67d76194 Merged r3613 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3662 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 15:24:49 +00:00
Jean-Philippe Lang eb55efd604 Merged r3612 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3661 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 15:23:30 +00:00
Jean-Philippe Lang 390eb7849c Merged r3611 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3660 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 15:21:08 +00:00
Jean-Philippe Lang 1588f3f6dc Merged r3397 from trunk to fix sqlite3 and pgsql test environments.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3658 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 15:06:43 +00:00
Jean-Philippe Lang 5ef8e8d45f Merged r3635 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3657 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:53:54 +00:00
Jean-Philippe Lang cd15eb77d9 Merged r3625 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3656 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:44:18 +00:00
Jean-Philippe Lang 3b699806d9 Merged r3614 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3655 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:43:29 +00:00
Jean-Philippe Lang d2367ccf53 Merged r3517 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3654 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:42:44 +00:00
Jean-Philippe Lang 9c00b8aa21 Merged r3530 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3653 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:41:59 +00:00
Jean-Philippe Lang bbb00b0bc4 Merged r3616 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3652 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:41:15 +00:00
Jean-Philippe Lang feac751318 Merged r3615 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3651 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:39:14 +00:00
Jean-Philippe Lang eb4a7f9f2e Merged r3566 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3650 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:37:59 +00:00
Jean-Philippe Lang 7ce9ba9e28 Merged r3562 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3649 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:34:05 +00:00
Jean-Philippe Lang 53880a4e08 Merged r3571 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3648 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:32:29 +00:00
Jean-Philippe Lang f92aa00705 Merged r3550 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3647 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:31:40 +00:00
Jean-Philippe Lang b240da3833 Merged r3532 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3646 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:30:50 +00:00
Jean-Philippe Lang da8624f7c7 Merged r3524 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3645 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:29:59 +00:00
Jean-Philippe Lang 46be83cd5e Merged r3537 and r3572 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3644 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:29:02 +00:00
Jean-Philippe Lang 667f7927a7 Merged r3494 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3643 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:21:47 +00:00
Jean-Philippe Lang 3e6f42e46d Backported r3608 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3642 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:20:40 +00:00
Jean-Philippe Lang 30c45a6187 Merged r3529 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3641 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:10:01 +00:00
Jean-Philippe Lang 32288ed5d7 Merged r3514 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3640 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:09:11 +00:00
Jean-Philippe Lang beb89eb8bb Merged r3558 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3639 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:08:03 +00:00
Jean-Philippe Lang 62d8016c4f Merged r3513 and r3531 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3638 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:06:10 +00:00
Jean-Philippe Lang 5a7a3b2392 Merged r3603 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3637 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:04:37 +00:00
Jean-Philippe Lang a2ad66a5c0 Merged r3539 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/0.9-stable@3636 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-04-11 14:03:28 +00:00
95 changed files with 483 additions and 184 deletions
+11 -6
View File
@@ -25,8 +25,7 @@ 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? if Setting.openid? && using_open_id?
@@ -39,9 +38,7 @@ class AccountController < ApplicationController
# Log out current user and redirect to welcome page # Log out current user and redirect to welcome page
def logout def logout
cookies.delete :autologin logout_user
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
@@ -134,7 +131,15 @@ class AccountController < ApplicationController
end end
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 password_authentication def password_authentication
user = User.try_to_login(params[:username], params[:password]) user = User.try_to_login(params[:username], params[:password])
+2 -1
View File
@@ -107,8 +107,9 @@ 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.downcase accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
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
+2 -2
View File
@@ -76,12 +76,12 @@ class EnumerationsController < ApplicationController
@enumeration.destroy @enumeration.destroy
redirect_to :action => 'index' redirect_to :action => 'index'
elsif params[:reassign_to_id] elsif params[:reassign_to_id]
if reassign_to = Enumeration.find_by_type_and_id(@enumeration.type, params[:reassign_to_id]) if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
@enumeration.destroy(reassign_to) @enumeration.destroy(reassign_to)
redirect_to :action => 'index' redirect_to :action => 'index'
end end
end end
@enumerations = Enumeration.find(:all, :conditions => ['type = (?)', @enumeration.type]) - [@enumeration] @enumerations = @enumeration.class.find(:all) - [@enumeration]
#rescue #rescue
# flash[:error] = 'Unable to delete enumeration' # flash[:error] = 'Unable to delete enumeration'
# redirect_to :action => 'index' # redirect_to :action => 'index'
+8 -12
View File
@@ -149,7 +149,7 @@ class IssuesController < ApplicationController
if request.get? || request.xhr? if request.get? || request.xhr?
@issue.start_date ||= Date.today @issue.start_date ||= Date.today
else else
requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) if params[:issue]
# Check that the user is allowed to apply the requested status # Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_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 })
@@ -223,10 +223,13 @@ class IssuesController < ApplicationController
user = @issue.author user = @issue.author
text = @issue.description text = @issue.description
end end
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " # Replaces pre blocks with [...]
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" 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| render(:update) { |page|
page.<< "$('notes').value = \"#{content}\";" page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update' page.show 'update'
page << "Form.Element.focus('notes');" page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');" page << "Element.scrollTo('update');"
@@ -280,14 +283,7 @@ class IssuesController < ApplicationController
def move def move
@copy = params[:copy_options] && params[:copy_options][:copy] @copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = [] @allowed_projects = Issue.allowed_target_projects_on_move
# 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 = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project @target_project ||= @project
@trackers = @target_project.trackers @trackers = @target_project.trackers
+7 -1
View File
@@ -38,6 +38,7 @@ 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()'
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") } members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
} }
} }
@@ -51,6 +52,7 @@ 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}")
} }
} }
@@ -64,7 +66,11 @@ 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| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } format.js { render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page << 'hideOnLoad()'
}
}
end end
end end
+15 -13
View File
@@ -27,7 +27,7 @@ class ProjectsController < ApplicationController
before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ] before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
before_filter :authorize_global, :only => :add 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_key_auth :activity accept_key_auth :activity, :index
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
if controller.request.post? if controller.request.post?
@@ -102,18 +102,20 @@ class ProjectsController < ApplicationController
redirect_to :controller => 'admin', :action => 'projects' redirect_to :controller => 'admin', :action => 'projects'
end end
else else
@project = Project.new(params[:project]) Mailer.with_deliveries(params[:notifications] == '1') do
@project.enabled_module_names = params[:enabled_modules] @project = Project.new(params[:project])
if validate_parent_id && @project.copy(@source_project, :only => params[:only]) @project.enabled_module_names = params[:enabled_modules]
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') if validate_parent_id && @project.copy(@source_project, :only => params[:only])
flash[:notice] = l(:notice_successful_create) @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
redirect_to :controller => 'admin', :action => 'projects' flash[:notice] = l(:notice_successful_create)
elsif !@project.new_record? redirect_to :controller => 'admin', :action => 'projects'
# Project was created elsif !@project.new_record?
# But some objects were not copied due to validation failures # Project was created
# (eg. issues from disabled trackers) # But some objects were not copied due to validation failures
# TODO: inform about that # (eg. issues from disabled trackers)
redirect_to :controller => 'admin', :action => 'projects' # TODO: inform about that
redirect_to :controller => 'admin', :action => 'projects'
end
end end
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
+1 -1
View File
@@ -74,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]
User.current.allowed_to?(:save_queries, @project, :global => true) render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+2 -2
View File
@@ -61,7 +61,7 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "subproject" when "subproject"
@field = "project_id" @field = "project_id"
@rows = @project.descendants.active @rows = @project.descendants.visible
@data = issues_by_subproject @data = issues_by_subproject
@report_title = l(:field_subproject) @report_title = l(:field_subproject)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
@@ -72,7 +72,7 @@ class ReportsController < ApplicationController
@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.active @subprojects = @project.descendants.visible
issues_by_tracker issues_by_tracker
issues_by_version issues_by_version
issues_by_priority issues_by_priority
+10 -5
View File
@@ -25,6 +25,9 @@ class VersionsController < ApplicationController
helper :projects helper :projects
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 end
def edit def edit
@@ -46,11 +49,13 @@ class VersionsController < ApplicationController
end end
def destroy def destroy
@version.destroy if @version.fixed_issues.empty?
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project @version.destroy
rescue redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
flash[:error] = l(:notice_unable_delete_version) else
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project flash[:error] = l(:notice_unable_delete_version)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
end end
def status_by def status_by
+20 -14
View File
@@ -52,17 +52,19 @@ module RepositoriesHelper
else else
change change
end end
end.compact end.compact
tree = { } tree = { }
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[dir] ||= {} p[path] ||= {}
p = p[dir] p = p[path]
end end
p[:c] = change p[:c] = change
end end
@@ -76,21 +78,26 @@ module RepositoriesHelper
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'
style << ' folder' if s text = File.basename(h(file))
style << " change-#{c.action}" if c if s = tree[file][:s]
style << ' folder'
text = h(file) path_param = to_path_param(@repository.relative_path(file))
unless c.nil? text = link_to(text, :controller => 'repositories',
:action => 'show',
:id => @project,
:path => path_param,
:rev => @changeset.revision)
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.revision) unless s || c.action == 'D' :rev => @changeset.revision) unless 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',
@@ -98,9 +105,8 @@ module RepositoriesHelper
:path => path_param, :path => path_param,
:rev => @changeset.revision) + ') ' 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
+6 -1
View File
@@ -182,6 +182,11 @@ class Changeset < ActiveRecord::Base
end end
end end
# removes invalid UTF8 sequences # removes invalid UTF8 sequences
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3] begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str
end
end end
end end
+21 -5
View File
@@ -80,7 +80,7 @@ class Issue < ActiveRecord::Base
end end
def copy_from(arg) def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.find(arg) issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on") self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
self.custom_values = issue.custom_values.collect {|v| v.clone} self.custom_values = issue.custom_values.collect {|v| v.clone}
self.status = issue.status self.status = issue.status
@@ -92,7 +92,7 @@ class Issue < ActiveRecord::Base
def move_to(new_project, new_tracker = nil, options = {}) def move_to(new_project, new_tracker = nil, options = {})
options ||= {} options ||= {}
issue = options[:copy] ? self.clone : self issue = options[:copy] ? self.clone : self
transaction do ret = Issue.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?
@@ -129,12 +129,12 @@ class Issue < ActiveRecord::Base
# 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})
end end
true
else else
Issue.connection.rollback_db_transaction raise ActiveRecord::Rollback
return false
end end
end end
return issue ret ? issue : false
end end
def priority_id=(pid) def priority_id=(pid)
@@ -389,6 +389,22 @@ 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
# 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
# Update issues so their versions are not pointing to a # Update issues so their versions are not pointing to a
+1 -1
View File
@@ -49,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) @user = User.find_by_mail(sender_email) if sender_email.present?
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
+11 -2
View File
@@ -114,11 +114,11 @@ class Mailer < ActionMailer::Base
when 'Project' when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :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)} recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version' when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) 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)} recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
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}"
@@ -310,6 +310,15 @@ class Mailer < ActionMailer::Base
deliver_reminder(assignee, issues, days) unless assignee.nil? 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)
+1 -1
View File
@@ -16,7 +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 Principal < ActiveRecord::Base class Principal < ActiveRecord::Base
set_table_name 'users' set_table_name "#{table_name_prefix}users#{table_name_suffix}"
has_many :members, :foreign_key => 'user_id', :dependent => :destroy has_many :members, :foreign_key => 'user_id', :dependent => :destroy
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name" has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
+4 -8
View File
@@ -1,5 +1,5 @@
# redMine - project management software # Redmine - project management software
# Copyright (C) 2006 Jean-Philippe Lang # Copyright (C) 2006-2010 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,10 +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 Version < ActiveRecord::Base class Version < ActiveRecord::Base
before_destroy :check_integrity
after_update :update_issues_from_sharing_change after_update :update_issues_from_sharing_change
belongs_to :project belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id' has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
acts_as_customizable acts_as_customizable
acts_as_attachable :view_permission => :view_files, acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files :delete_permission => :manage_files
@@ -155,10 +154,7 @@ class Version < ActiveRecord::Base
end end
end end
private private
def check_integrity
raise "Can't delete version" if self.fixed_issues.find(:first)
end
# Update the issue's fixed versions. Used if a version's sharing changes. # Update the issue's fixed versions. Used if a version's sharing changes.
def update_issues_from_sharing_change def update_issues_from_sharing_change
+2
View File
@@ -1,3 +1,4 @@
<%= call_hook :view_account_login_top %>
<div id="login-form"> <div id="login-form">
<% form_tag({:action=> "login"}) do %> <% form_tag({:action=> "login"}) do %>
<%= back_url_hidden_field_tag %> <%= back_url_hidden_field_tag %>
@@ -38,3 +39,4 @@
<%= javascript_tag "Form.Element.focus('username');" %> <%= javascript_tag "Form.Element.focus('username');" %>
<% end %> <% end %>
</div> </div>
<%= call_hook :view_account_login_bottom %>
+4
View File
@@ -26,3 +26,7 @@
<% end %> <% end %>
<% html_title @document.title -%> <% html_title @document.title -%>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<% end %>
+1 -1
View File
@@ -29,7 +29,7 @@
<% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %> <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_user_new)%></legend> <fieldset><legend><%=l(:label_user_new)%></legend>
<p><%= text_field_tag 'user_search', nil, :size => "40" %></p> <p><%= text_field_tag 'user_search', nil %></p>
<%= observe_field(:user_search, <%= observe_field(:user_search,
:frequency => 0.5, :frequency => 0.5,
:update => :users, :update => :users,
+1 -1
View File
@@ -26,7 +26,7 @@
<% end %> <% end %>
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>"> <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
<td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> <td class="id"><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
</tr> </tr>
<% end -%> <% end -%>
+4 -4
View File
@@ -10,10 +10,10 @@
<% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project %> <%= hidden_field_tag('project_id', @project.to_param) if @project %>
<div id="query_form_content"> <div id="query_form_content" class="hide-when-print">
<fieldset id="filters" class="collapsible"> <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div> <div style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %> <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div> </div>
</fieldset> </fieldset>
@@ -33,7 +33,7 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<p class="buttons"> <p class="buttons hide-when-print">
<%= link_to_remote l(:button_apply), <%= link_to_remote l(:button_apply),
{ :url => { :set_filter => 1 }, { :url => { :set_filter => 1 },
+2
View File
@@ -40,9 +40,11 @@
<h1><%= page_header_title %></h1> <h1><%= page_header_title %></h1>
<% if display_main_menu?(@project) %>
<div id="main-menu"> <div id="main-menu">
<%= render_main_menu(@project) %> <%= render_main_menu(@project) %>
</div> </div>
<% end %>
</div> </div>
<%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %> <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
+2 -2
View File
@@ -17,10 +17,10 @@
<% if Setting.rest_api_enabled? %> <% if Setting.rest_api_enabled? %>
<h4><%= l(:label_api_access_key) %></h4> <h4><%= l(:label_api_access_key) %></h4>
<p> <div>
<%= link_to_function(l(:button_show), "$('api-access-key').toggle();")%> <%= link_to_function(l(:button_show), "$('api-access-key').toggle();")%>
<pre id='api-access-key' class='autoscroll'><%= @user.api_key %></pre> <pre id='api-access-key' class='autoscroll'><%= @user.api_key %></pre>
</p> </div>
<%= javascript_tag("$('api-access-key').hide();") %> <%= javascript_tag("$('api-access-key').hide();") %>
<p> <p>
<% if @user.api_token %> <% if @user.api_token %>
+2
View File
@@ -21,6 +21,8 @@
<label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label> <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
<label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label> <label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label>
<%= hidden_field_tag 'only[]', '' %> <%= hidden_field_tag 'only[]', '' %>
<br />
<label class="block"><%= check_box_tag 'notifications', 1, params[:notifications] %> <%= l(:label_project_copy_notifications) %></label>
</fieldset> </fieldset>
<%= submit_tag l(:button_copy) %> <%= submit_tag l(:button_copy) %>
+4
View File
@@ -1,3 +1,7 @@
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
<% end %>
<div class="contextual"> <div class="contextual">
<%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %> <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
<%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %> <%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
+1 -1
View File
@@ -58,7 +58,7 @@
<% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %> <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_member_new)%></legend> <fieldset><legend><%=l(:label_member_new)%></legend>
<p><%= text_field_tag 'principal_search', nil, :size => "40" %></p> <p><%= text_field_tag 'principal_search', nil %></p>
<%= observe_field(:principal_search, <%= observe_field(:principal_search,
:frequency => 0.5, :frequency => 0.5,
:update => :principals, :update => :principals,
+3 -1
View File
@@ -7,7 +7,9 @@
<h2><%=l(:label_overview)%></h2> <h2><%=l(:label_overview)%></h2>
<div class="splitcontentleft"> <div class="splitcontentleft">
<%= textilizable @project.description %> <div class="wiki">
<%= textilizable @project.description %>
</div>
<ul> <ul>
<% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)) %></li><% end %> <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)) %></li><% end %>
<% if @subprojects.any? %> <% if @subprojects.any? %>
+1 -1
View File
@@ -16,6 +16,6 @@ dirs.each do |dir|
/ <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %> / <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
<% end %> <% end %>
<%= "@ #{revision}" if revision %> <%= "@ #{h revision}" if revision %>
<% html_title(with_leading_slash(path)) -%> <% html_title(with_leading_slash(path)) -%>
+2 -5
View File
@@ -32,13 +32,10 @@
<%= render :partial => 'versions/overview', :locals => {:version => @version} %> <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %> <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
<% issues = @version.fixed_issues.find(:all, <% if @issues.present? %>
:include => [:status, :tracker, :priority],
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
<% if issues.size > 0 %>
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend> <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
<ul> <ul>
<% issues.each do |issue| -%> <% @issues.each do |issue| -%>
<li><%= link_to_issue(issue) %></li> <li><%= link_to_issue(issue) %></li>
<% end -%> <% end -%>
</ul> </ul>
+1 -29
View File
@@ -1,29 +1 @@
# Settings specified here will take precedence over those in config/environment.rb instance_eval File.read(File.join(File.dirname(__FILE__), 'test.rb'))
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :test
config.action_controller.session = {
:session_key => "_test_session",
:secret => "some secret phrase for the tests."
}
# Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
config.action_controller.allow_forgery_protection = false
config.gem "thoughtbot-shoulda", :lib => "shoulda", :source => "http://gems.github.com"
config.gem "nofxx-object_daddy", :lib => "object_daddy", :source => "http://gems.github.com"
config.gem "mocha"
+1 -29
View File
@@ -1,29 +1 @@
# Settings specified here will take precedence over those in config/environment.rb instance_eval File.read(File.join(File.dirname(__FILE__), 'test.rb'))
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :test
config.action_controller.session = {
:session_key => "_test_session",
:secret => "some secret phrase for the tests."
}
# Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
config.action_controller.allow_forgery_protection = false
config.gem "thoughtbot-shoulda", :lib => "shoulda", :source => "http://gems.github.com"
config.gem "nofxx-object_daddy", :lib => "object_daddy", :source => "http://gems.github.com"
config.gem "mocha"
+1 -1
View File
@@ -881,4 +881,4 @@ bg:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -905,3 +905,4 @@ bs:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -884,3 +884,4 @@ ca:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -887,3 +887,4 @@ cs:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -907,3 +907,4 @@ da:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -907,3 +907,4 @@ de:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -887,3 +887,4 @@ el:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -748,6 +748,7 @@ en:
label_api_access_key: API access key label_api_access_key: API access key
label_missing_api_access_key: Missing an API access key label_missing_api_access_key: Missing an API access key
label_api_access_key_created_on: "API access key created {{value}} ago" label_api_access_key_created_on: "API access key created {{value}} ago"
label_project_copy_notifications: Send email notifications during the project copy
button_login: Login button_login: Login
button_submit: Submit button_submit: Submit
+1
View File
@@ -931,3 +931,4 @@ es:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -917,3 +917,4 @@ fi:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+3 -2
View File
@@ -764,6 +764,7 @@ fr:
label_missing_feeds_access_key: Clé d'accès RSS manquante label_missing_feeds_access_key: Clé d'accès RSS manquante
label_close_versions: Fermer les versions terminées label_close_versions: Fermer les versions terminées
label_revision_id: Revision {{value}} label_revision_id: Revision {{value}}
label_project_copy_notifications: Envoyer les notifications durant la copie du projet
button_login: Connexion button_login: Connexion
button_submit: Soumettre button_submit: Soumettre
@@ -811,10 +812,10 @@ fr:
status_active: actif status_active: actif
status_registered: enregistré status_registered: enregistré
status_locked: vérouillé status_locked: verrouillé
version_status_open: ouvert version_status_open: ouvert
version_status_locked: vérouillé version_status_locked: verrouillé
version_status_closed: fermé version_status_closed: fermé
text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
+1
View File
@@ -907,3 +907,4 @@ gl:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -891,3 +891,4 @@ he:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -894,3 +894,4 @@ hr:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -912,3 +912,4 @@
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -899,3 +899,4 @@ id:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1 -1
View File
@@ -894,4 +894,4 @@ it:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -916,3 +916,4 @@ ja:
enumeration_activities: 作業分類 (時間トラッキング) enumeration_activities: 作業分類 (時間トラッキング)
enumeration_system_activity: システム作業分類 enumeration_system_activity: システム作業分類
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -947,3 +947,4 @@ ko:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -955,3 +955,4 @@ lt:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -869,3 +869,4 @@ nl:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -882,3 +882,4 @@
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -912,3 +912,4 @@ pl:
permission_export_wiki_pages: Eksport stron wiki permission_export_wiki_pages: Eksport stron wiki
permission_manage_project_activities: Zarządzanie aktywnościami projektu permission_manage_project_activities: Zarządzanie aktywnościami projektu
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -915,3 +915,4 @@ pt-BR:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -899,3 +899,4 @@ pt:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -884,3 +884,4 @@ ro:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -995,3 +995,4 @@ ru:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -886,3 +886,4 @@ sk:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -883,3 +883,4 @@ sl:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -902,3 +902,4 @@
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -936,3 +936,4 @@ sv:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -884,3 +884,4 @@ th:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -914,3 +914,4 @@ tr:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -883,3 +883,4 @@ uk:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -946,3 +946,4 @@ vi:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1
View File
@@ -978,3 +978,4 @@
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+1 -1
View File
@@ -909,4 +909,4 @@ zh:
permission_export_wiki_pages: Export wiki pages permission_export_wiki_pages: Export wiki pages
setting_cache_formatted_text: Cache formatted text setting_cache_formatted_text: Cache formatted text
permission_manage_project_activities: Manage project activities permission_manage_project_activities: Manage project activities
label_project_copy_notifications: Send email notifications during the project copy
+56
View File
@@ -4,6 +4,62 @@ Redmine - project management software
Copyright (C) 2006-2010 Jean-Philippe Lang Copyright (C) 2006-2010 Jean-Philippe Lang
http://www.redmine.org/ http://www.redmine.org/
== v1.0.0
Adds context menu to the roadmap issue lists
== 2010-07-07 v0.9.6
Fixed: Redmine.pm access by unauthorized users
== 2010-06-24 v0.9.5
Linkify folder names on revision view
"fiters" and "options" should be hidden in print view via css
Fixed: NoMethodError when no issue params are submitted
Fixed: projects.atom with required authentication
Fixed: External links not correctly displayed in Wiki TOC
Fixed: Member role forms in project settings are not hidden after member added
Fixed: pre can't be inside p
Fixed: session cookie path does not respect RAILS_RELATIVE_URL_ROOT
Fixed: mail handler fails when the from address is empty
== 2010-05-01 v0.9.4
Filters collapsed by default on issues index page for a saved query
Fixed: When categories list is too big the popup menu doesn't adjust (ex. in the issue list)
Fixed: remove "main-menu" div when the menu is empty
Fixed: Code syntax highlighting not working in Document page
Fixed: Git blame/annotate fails on moved files
Fixed: Failing test in test_show_atom
Fixed: Migrate from trac - not displayed Wikis
Fixed: Email notifications on file upload sent to empty recipient list
Fixed: Migrating from trac is not possible, fails to allocate memory
Fixed: Lost password no longer flashes a confirmation message
Fixed: Crash while deleting in-use enumeration
Fixed: Hard coded English string at the selection of issue watchers
Fixed: Bazaar v2.1.0 changed behaviour
Fixed: Roadmap display can raise an exception if no trackers are selected
Fixed: Gravatar breaks layout of "logged in" page
Fixed: Reposman.rb on Windows
Fixed: Possible error 500 while moving an issue to another project with SQLite
Fixed: backslashes in issue description/note should be escaped when quoted
Fixed: Long text in <pre> disrupts Associated revisions
Fixed: Links to missing wiki pages not red on project overview page
Fixed: Cannot delete a project with subprojects that shares versions
Fixed: Update of Subversion changesets broken under Solaris
Fixed: "Move issues" permission not working for Non member
Fixed: Sidebar overlap on Users tab of Group editor
Fixed: Error on db:migrate with table prefix set (hardcoded name in principal.rb)
Fixed: Report shows sub-projects for non-members
Fixed: 500 internal error when browsing any Redmine page in epiphany
Fixed: Watchers selection lost when issue creation fails
Fixed: When copying projects, redmine should not generate an email to people who created issues
Fixed: Issue "#" table cells should have a class attribute to enable fine-grained CSS theme
Fixed: Plugin generators should display help if no parameter is given
== 2010-02-28 v0.9.3 == 2010-02-28 v0.9.3
Adds filter for system shared versions on the cross project issue list Adds filter for system shared versions on the cross project issue list
+1 -1
View File
@@ -33,7 +33,7 @@ Optional:
4. Generate a session store secret 4. Generate a session store secret
Redmine stores session data in cookies by default, which requires Redmine stores session data in cookies by default, which requires
a secret to be generated. Run: a secret to be generated. Run:
rake config/initializers/session_store.rb rake generate_session_store
5. Create the database structure. Under the application main directory: 5. Create the database structure. Under the application main directory:
rake db:migrate RAILS_ENV="production" rake db:migrate RAILS_ENV="production"
+2 -2
View File
@@ -12,12 +12,12 @@ http://www.redmine.org/
2. Copy your database settings (RAILS_ROOT/config/database.yml) 2. Copy your database settings (RAILS_ROOT/config/database.yml)
and SMTP settings (RAILS_ROOT/config/email.yml) and SMTP settings (RAILS_ROOT/config/email.yml)
into the new config directory into the new config directory
DO NOT REPLACE ANY OTHERS FILES. DO NOT REPLACE OR EDIT ANY OTHER FILES.
3. Generate a session store secret 3. Generate a session store secret
Redmine stores session data in cookies by default, which requires Redmine stores session data in cookies by default, which requires
a secret to be generated. Run: a secret to be generated. Run:
rake config/initializers/session_store.rb rake generate_session_store
4. Migrate your database (please make a backup before doing this): 4. Migrate your database (please make a backup before doing this):
rake db:migrate RAILS_ENV="production" rake db:migrate RAILS_ENV="production"
+32 -1
View File
@@ -227,9 +227,38 @@ sub authen_handler {
} }
} }
# check if authentication is forced
sub is_authentication_forced {
my $r = shift;
my $dbh = connect_database($r);
my $sth = $dbh->prepare(
"SELECT value FROM settings where settings.name = 'login_required';"
);
$sth->execute();
my $ret = 0;
if (my @row = $sth->fetchrow_array) {
if ($row[0] eq "1" || $row[0] eq "t") {
$ret = 1;
}
}
$sth->finish();
undef $sth;
$dbh->disconnect();
undef $dbh;
$ret;
}
sub is_public_project { sub is_public_project {
my $project_id = shift; my $project_id = shift;
my $r = shift; my $r = shift;
if (is_authentication_forced($r)) {
return 0;
}
my $dbh = connect_database($r); my $dbh = connect_database($r);
my $sth = $dbh->prepare( my $sth = $dbh->prepare(
@@ -309,7 +338,9 @@ sub is_member {
bindpw => $rowldap[4] ? $rowldap[4] : "", bindpw => $rowldap[4] ? $rowldap[4] : "",
filter => "(".$rowldap[6]."=%s)" filter => "(".$rowldap[6]."=%s)"
); );
$ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass)); my $method = $r->method;
$ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
} }
$sthldap->finish(); $sthldap->finish();
undef $sthldap; undef $sthldap;
+6 -2
View File
@@ -221,10 +221,14 @@ def other_read_right?(file)
end end
def owner_name(file) def owner_name(file)
RUBY_PLATFORM =~ /mswin/ ? mswin? ?
$svn_owner : $svn_owner :
Etc.getpwuid( File.stat(file).uid ).name Etc.getpwuid( File.stat(file).uid ).name
end end
def mswin?
(RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
end
projects.each do |project| projects.each do |project|
log("treating project #{project.name}", :level => 1) log("treating project #{project.name}", :level => 1)
@@ -303,4 +307,4 @@ projects.each do |project|
end end
end end
@@ -6,6 +6,7 @@ class RedminePluginControllerGenerator < ControllerGenerator
def initialize(runtime_args, runtime_options = {}) def initialize(runtime_args, runtime_options = {})
runtime_args = runtime_args.dup runtime_args = runtime_args.dup
usage if runtime_args.empty?
@plugin_name = "redmine_" + runtime_args.shift.underscore @plugin_name = "redmine_" + runtime_args.shift.underscore
@plugin_pretty_name = plugin_name.titleize @plugin_pretty_name = plugin_name.titleize
@plugin_path = "vendor/plugins/#{plugin_name}" @plugin_path = "vendor/plugins/#{plugin_name}"
@@ -6,6 +6,7 @@ class RedminePluginModelGenerator < ModelGenerator
def initialize(runtime_args, runtime_options = {}) def initialize(runtime_args, runtime_options = {})
runtime_args = runtime_args.dup runtime_args = runtime_args.dup
usage if runtime_args.empty?
@plugin_name = "redmine_" + runtime_args.shift.underscore @plugin_name = "redmine_" + runtime_args.shift.underscore
@plugin_pretty_name = plugin_name.titleize @plugin_pretty_name = plugin_name.titleize
@plugin_path = "vendor/plugins/#{plugin_name}" @plugin_path = "vendor/plugins/#{plugin_name}"
+1 -1
View File
@@ -818,7 +818,7 @@ class RedCloth3 < String
post = ")"+post # add closing parenth to post post = ")"+post # add closing parenth to post
end end
atts = pba( atts ) atts = pba( atts )
atts = " href=\"#{ url }#{ slash }\"#{ atts }" atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
atts << " title=\"#{ htmlesc title }\"" if title atts << " title=\"#{ htmlesc title }\"" if title
atts = shelve( atts ) if atts atts = shelve( atts ) if atts
+5
View File
@@ -166,6 +166,11 @@ module Redmine
render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project) render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
end end
def display_main_menu?(project)
menu_name = project && !project.new_record? ? :project_menu : :application_menu
Redmine::MenuManager.items(menu_name).size > 1 # 1 element is the root
end
def render_menu(menu, project=nil) def render_menu(menu, project=nil)
links = [] links = []
menu_items_for(menu, project) do |node| menu_items_for(menu, project) do |node|
+5 -5
View File
@@ -56,14 +56,14 @@ module Redmine
shellout(cmd) do |io| shellout(cmd) do |io|
prefix = "#{url}/#{path}".gsub('\\', '/') prefix = "#{url}/#{path}".gsub('\\', '/')
logger.debug "PREFIX: #{prefix}" logger.debug "PREFIX: #{prefix}"
re = %r{^V\s+#{Regexp.escape(prefix)}(\/?)([^\/]+)(\/?)\s+(\S+)$} re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)$}
io.each_line do |line| io.each_line do |line|
next unless line =~ re next unless line =~ re
entries << Entry.new({:name => $2.strip, entries << Entry.new({:name => $3.strip,
:path => ((path.empty? ? "" : "#{path}/") + $2.strip), :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
:kind => ($3.blank? ? 'file' : 'dir'), :kind => ($4.blank? ? 'file' : 'dir'),
:size => nil, :size => nil,
:lastrev => Revision.new(:revision => $4.strip) :lastrev => Revision.new(:revision => $5.strip)
}) })
end end
end end
+13 -3
View File
@@ -227,16 +227,26 @@ module Redmine
def annotate(path, identifier=nil) def annotate(path, identifier=nil)
identifier = 'HEAD' if identifier.blank? identifier = 'HEAD' if identifier.blank?
cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}" cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
blame = Annotate.new blame = Annotate.new
content = nil content = nil
shellout(cmd) { |io| io.binmode; content = io.read } shellout(cmd) { |io| io.binmode; content = io.read }
return nil if $? && $?.exitstatus != 0 return nil if $? && $?.exitstatus != 0
# git annotates binary files # git annotates binary files
return nil if content.is_binary_data? return nil if content.is_binary_data?
identifier = ''
# git shows commit author on the first occurrence only
authors_by_commit = {}
content.split("\n").each do |line| content.split("\n").each do |line|
next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/ if line =~ /^([0-9a-f]{39,40})\s.*/
blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip)) identifier = $1
elsif line =~ /^author (.+)/
authors_by_commit[identifier] = $1.strip
elsif line =~ /^\t(.*)/
blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
identifier = ''
author = ''
end
end end
blame blame
end end
+1 -1
View File
@@ -4,7 +4,7 @@ module Redmine
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 0 MAJOR = 0
MINOR = 9 MINOR = 9
TINY = 3 TINY = 6
# Branch values: # Branch values:
# * official release: nil # * official release: nil
@@ -22,6 +22,7 @@ module Redmine
module WikiFormatting module WikiFormatting
module Textile module Textile
class Formatter < RedCloth3 class Formatter < RedCloth3
include ActionView::Helpers::TagHelper
# auto_link rule after textile rules so that it doesn't break !image_url! tags # auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
@@ -66,6 +67,11 @@ module Redmine
def textile_p_withtoc(tag, atts, cite, content) def textile_p_withtoc(tag, atts, cite, content)
# removes wiki links from the item # removes wiki links from the item
toc_item = content.gsub(/(\[\[([^\]\|]*)(\|([^\]]*))?\]\])/) { $4 || $2 } toc_item = content.gsub(/(\[\[([^\]\|]*)(\|([^\]]*))?\]\])/) { $4 || $2 }
# sanitizes titles from links
# see redcloth3.rb, same as "#{pre}#{text}#{post}"
toc_item.gsub!(LINK_RE) { [$2, $4, $9].join }
# sanitizes image links from titles
toc_item.gsub!(IMAGE_RE) { [$5].join }
# removes styles # removes styles
# eg. %{color:red}Triggers% => Triggers # eg. %{color:red}Triggers% => Triggers
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
@@ -162,7 +168,8 @@ module Redmine
url=url[0..-2] # discard closing parenth from url url=url[0..-2] # discard closing parenth from url
post = ")"+post # add closing parenth to post post = ")"+post # add closing parenth to post
end end
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post}) tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
%(#{leading}#{tag}#{post})
end end
end end
end end
@@ -174,7 +181,7 @@ module Redmine
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
mail mail
else else
%{<a href="mailto:#{mail}" class="email">#{mail}</a>} content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
end end
end end
end end
+7
View File
@@ -17,6 +17,13 @@ file 'config/initializers/session_store.rb' do
# you'll be exposed to dictionary attacks. # you'll be exposed to dictionary attacks.
ActionController::Base.session = { ActionController::Base.session = {
:session_key => '_redmine_session', :session_key => '_redmine_session',
#
# Uncomment and edit the :session_path below if are hosting your Redmine
# at a suburi and don't want the top level path to access the cookies
#
# See: http://www.redmine.org/issues/3968
#
# :session_path => '/url_path_to/your/redmine/',
:secret => '#{secret}' :secret => '#{secret}'
} }
EOF EOF
+2 -1
View File
@@ -310,7 +310,7 @@ namespace :redmine do
# Ticket number re-writing # Ticket number re-writing
text = text.gsub(/#(\d+)/) do |s| text = text.gsub(/#(\d+)/) do |s|
if $1.length < 10 if $1.length < 10
TICKET_MAP[$1.to_i] ||= $1 # TICKET_MAP[$1.to_i] ||= $1
"\##{TICKET_MAP[$1.to_i] || $1}" "\##{TICKET_MAP[$1.to_i] || $1}"
else else
s s
@@ -682,6 +682,7 @@ namespace :redmine do
project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG) project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE) project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
@target_project = project.new_record? ? nil : project @target_project = project.new_record? ? nil : project
@target_project.reload
end end
def self.connection_params def self.connection_params
+9
View File
@@ -207,3 +207,12 @@ Ajax.Responders.register({
} }
} }
}); });
function hideOnLoad() {
$$('.hol').each(function(el) {
el.hide();
});
}
Event.observe(window, 'load', hideOnLoad);
+8 -5
View File
@@ -399,6 +399,8 @@ div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-h
table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
input#principal_search, input#user_search {width:100%}
* html div#tab-content-members fieldset div { height: 450px; } * html div#tab-content-members fieldset div { height: 450px; }
/***** Flash & error messages ****/ /***** Flash & error messages ****/
@@ -640,7 +642,7 @@ div.wiki pre {
padding: 2px; padding: 2px;
background-color: #fafafa; background-color: #fafafa;
border: 1px solid #dadada; border: 1px solid #dadada;
width:95%; width:auto;
overflow-x: auto; overflow-x: auto;
} }
@@ -815,14 +817,14 @@ div.issue table img.gravatar {
h2 img.gravatar { h2 img.gravatar {
padding: 3px; padding: 3px;
margin: -2px 4px 0 0; margin: -2px 4px -4px 0;
float: left; vertical-align: top;
} }
h4 img.gravatar { h4 img.gravatar {
padding: 3px; padding: 3px;
margin: -6px 4px 0 0; margin: -6px 0 -4px 0;
float: left; vertical-align: top;
} }
td.username img.gravatar { td.username img.gravatar {
@@ -850,4 +852,5 @@ h2 img { vertical-align:middle; }
#main { background: #fff; } #main { background: #fff; }
#content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
#wiki_add_attachment { display:none; } #wiki_add_attachment { display:none; }
.hide-when-print { display: none; }
} }
+1 -1
View File
@@ -22,7 +22,7 @@
padding:1px; padding:1px;
z-index:39; z-index:39;
} }
#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; } #context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; max-height:300px; overflow:hidden; overflow-y: auto; }
#context-menu li.folder>ul { left:148px; } #context-menu li.folder>ul { left:148px; }
#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; } #context-menu.reverse-y li.folder>ul { top:auto; bottom:0; }
+17
View File
@@ -0,0 +1,17 @@
Return-Path: <john.doe@somenet.foo>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
To: <redmine@somenet.foo>
Subject: Ticket by unknown user
Date: Sun, 22 Jun 2008 12:28:07 +0200
MIME-Version: 1.0
Content-Type: text/plain;
format=flowed;
charset="iso-8859-1";
reply-type=original
Content-Transfer-Encoding: 7bit
This is a ticket submitted by an unknown user.
+7 -1
View File
@@ -436,7 +436,7 @@ class IssuesControllerTest < ActionController::TestCase
assert_response :success assert_response :success
assert_template 'changes.rxml' assert_template 'changes.rxml'
# Inline image # Inline image
assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}" assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
end end
def test_new_routing def test_new_routing
@@ -649,6 +649,12 @@ class IssuesControllerTest < ActionController::TestCase
:value => 'Value for field 2'} :value => 'Value for field 2'}
end end
test "POST new with no issue params" do
@request.session[:user_id] = 2
post :new, :project_id => 1
assert_response :success
end
def test_copy_routing def test_copy_routing
assert_routing( assert_routing(
{:method => :get, :path => '/projects/world_domination/issues/567/copy'}, {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
+15
View File
@@ -15,6 +15,21 @@ class GitAdapterTest < ActiveSupport::TestCase
def test_getting_all_revisions def test_getting_all_revisions
assert_equal 12, @adapter.revisions('',nil,nil,:all => true).length assert_equal 12, @adapter.revisions('',nil,nil,:all => true).length
end end
def test_annotate
annotate = @adapter.annotate('sources/watchers_controller.rb')
assert_kind_of Redmine::Scm::Adapters::Annotate, annotate
assert_equal 41, annotate.lines.size
assert_equal "# This program is free software; you can redistribute it and/or", annotate.lines[4].strip
assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", annotate.revisions[4].identifier
assert_equal "jsmith", annotate.revisions[4].author
end
def test_annotate_moved_file
annotate = @adapter.annotate('renamed_test.txt')
assert_kind_of Redmine::Scm::Adapters::Annotate, annotate
assert_equal 2, annotate.lines.size
end
else else
puts "Git test repository NOT FOUND. Skipping unit tests !!!" puts "Git test repository NOT FOUND. Skipping unit tests !!!"
def test_fake; assert true end def test_fake; assert true end
+12 -2
View File
@@ -59,12 +59,14 @@ class ApplicationHelperTest < HelperTestCase
'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>', 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
# two exclamation marks # two exclamation marks
'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>', 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
# escaping
'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
} }
to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
end end
def test_auto_mailto def test_auto_mailto
assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>', assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
textilizable('test@foo.bar') textilizable('test@foo.bar')
end end
@@ -129,6 +131,8 @@ RAW
"\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>", "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
# two exclamation marks # two exclamation marks
'"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>', '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
# escaping
'"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
} }
to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
end end
@@ -360,6 +364,10 @@ h2. Subtitle with %{color:red}red text%
h1. Another title h1. Another title
h2. An "Internet link":http://www.redmine.org/ inside subtitle
h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
RAW RAW
expected = '<ul class="toc">' + expected = '<ul class="toc">' +
@@ -368,8 +376,10 @@ RAW
'<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' + '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
'<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' + '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
'<li class="heading1"><a href="#Another-title">Another title</a></li>' + '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
'<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
'<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
'</ul>' '</ul>'
assert textilizable(raw).gsub("\n", "").include?(expected) assert textilizable(raw).gsub("\n", "").include?(expected)
end end
+26
View File
@@ -386,6 +386,16 @@ class IssueTest < ActiveSupport::TestCase
assert_equal 7, issue.fixed_version_id assert_equal 7, issue.fixed_version_id
end end
def test_move_to_another_project_with_disabled_tracker
issue = Issue.find(1)
target = Project.find(2)
target.tracker_ids = [3]
target.save
assert_equal false, issue.move_to(target)
issue.reload
assert_equal 1, issue.project_id
end
def test_copy_to_the_same_project def test_copy_to_the_same_project
issue = Issue.find(1) issue = Issue.find(1)
copy = nil copy = nil
@@ -589,4 +599,20 @@ class IssueTest < ActiveSupport::TestCase
end end
end end
end end
context ".allowed_target_projects_on_move" do
should "return all active projects for admin users" do
User.current = User.find(1)
assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
end
should "return allowed projects for non admin users" do
User.current = User.find(2)
Role.non_member.remove_permission! :move_issues
assert_equal 3, Issue.allowed_target_projects_on_move.size
Role.non_member.add_permission! :move_issues
assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
end
end
end end
+9
View File
@@ -166,6 +166,15 @@ class MailHandlerTest < ActiveSupport::TestCase
assert issue.author.anonymous? assert issue.author.anonymous?
end end
end end
def test_add_issue_by_anonymous_user_with_no_from_address
Role.anonymous.add_permission!(:add_issues)
assert_no_difference 'User.count' do
issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
assert issue.is_a?(Issue)
assert issue.author.anonymous?
end
end
def test_add_issue_by_anonymous_user_on_private_project def test_add_issue_by_anonymous_user_on_private_project
Role.anonymous.add_permission!(:add_issues) Role.anonymous.add_permission!(:add_issues)
+37
View File
@@ -21,6 +21,12 @@ class MailerTest < ActiveSupport::TestCase
include Redmine::I18n include Redmine::I18n
include ActionController::Assertions::SelectorAssertions include ActionController::Assertions::SelectorAssertions
fixtures :projects, :enabled_modules, :issues, :users, :members, :member_roles, :roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories fixtures :projects, :enabled_modules, :issues, :users, :members, :member_roles, :roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
def setup
ActionMailer::Base.deliveries.clear
Setting.host_name = 'mydomain.foo'
Setting.protocol = 'http'
end
def test_generated_links_in_emails def test_generated_links_in_emails
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
@@ -231,6 +237,20 @@ class MailerTest < ActiveSupport::TestCase
end end
end end
def test_version_file_added
attachements = [ Attachment.find_by_container_type('Version') ]
assert Mailer.deliver_attachments_added(attachements)
assert_not_nil last_email.bcc
assert last_email.bcc.any?
end
def test_project_file_added
attachements = [ Attachment.find_by_container_type('Project') ]
assert Mailer.deliver_attachments_added(attachements)
assert_not_nil last_email.bcc
assert last_email.bcc.any?
end
def test_news_added def test_news_added
news = News.find(:first) news = News.find(:first)
valid_languages.each do |lang| valid_languages.each do |lang|
@@ -282,6 +302,14 @@ class MailerTest < ActiveSupport::TestCase
end end
end end
def test_test
user = User.find(1)
valid_languages.each do |lang|
user.update_attribute :language, lang.to_s
assert Mailer.deliver_test(user)
end
end
def test_reminders def test_reminders
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
Mailer.reminders(:days => 42) Mailer.reminders(:days => 42)
@@ -310,4 +338,13 @@ class MailerTest < ActiveSupport::TestCase
assert_equal :it, current_language assert_equal :it, current_language
end end
def test_with_deliveries_off
Mailer.with_deliveries false do
Mailer.deliver_test(User.find(1))
end
assert ActionMailer::Base.deliveries.empty?
# should restore perform_deliveries
assert ActionMailer::Base.perform_deliveries
end
end end
+6
View File
@@ -47,6 +47,12 @@ class WatcherTest < ActiveSupport::TestCase
assert Issue.watched_by(@user).include?(@issue) assert Issue.watched_by(@user).include?(@issue)
end end
def test_watcher_user_ids
issue = Issue.new
issue.watcher_user_ids = ['1', '3']
assert issue.watched_by?(User.find(1))
end
def test_recipients def test_recipients
@issue.watchers.delete_all @issue.watchers.delete_all
@issue.reload @issue.reload
+2 -2
View File
@@ -46,9 +46,9 @@ module Redmine
watching ? add_watcher(user) : remove_watcher(user) watching ? add_watcher(user) : remove_watcher(user)
end end
# Returns true if object is watched by user # Returns true if object is watched by +user+
def watched_by?(user) def watched_by?(user)
!!(user && self.watchers.detect {|w| w.user_id == user.id }) !!(user && self.watcher_user_ids.detect {|uid| uid == user.id })
end end
# Returns an array of watchers' email addresses # Returns an array of watchers' email addresses