##// END OF EJS Templates
Custom fields refactoring: most of code moved from controllers to models (using new module ActsAsCustomizable)....
Jean-Philippe Lang -
r1578:ce6cf66f6c3a
parent child
Show More
@@ -0,0 +1,2
1 require File.dirname(__FILE__) + '/lib/acts_as_customizable'
2 ActiveRecord::Base.send(:include, Redmine::Acts::Customizable)
@@ -0,0 +1,82
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Acts
20 module Customizable
21 def self.included(base)
22 base.extend ClassMethods
23 end
24
25 module ClassMethods
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
29 self.customizable_options = options
30 has_many :custom_values, :dependent => :delete_all, :as => :customized
31 before_validation_on_create { |customized| customized.custom_field_values }
32 # Trigger validation only if custom values were changed
33 validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
34 send :include, Redmine::Acts::Customizable::InstanceMethods
35 # Save custom values when saving the customized object
36 after_save :save_custom_field_values
37 end
38 end
39
40 module InstanceMethods
41 def self.included(base)
42 base.extend ClassMethods
43 end
44
45 def available_custom_fields
46 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
47 :order => 'position')
48 end
49
50 def custom_field_values=(values)
51 @custom_field_values_changed = true
52 values = values.stringify_keys
53 custom_field_values.each do |custom_value|
54 custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
55 end if values.is_a?(Hash)
56 end
57
58 def custom_field_values
59 @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) }
60 end
61
62 def custom_field_values_changed?
63 @custom_field_values_changed == true
64 end
65
66 def custom_value_for(c)
67 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
68 custom_values.detect {|v| v.custom_field_id == field_id }
69 end
70
71 def save_custom_field_values
72 custom_field_values.each(&:save)
73 @custom_field_values_changed = false
74 @custom_field_values = nil
75 end
76
77 module ClassMethods
78 end
79 end
80 end
81 end
82 end
@@ -1,178 +1,173
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AccountController < ApplicationController
18 class AccountController < ApplicationController
19 layout 'base'
19 layout 'base'
20 helper :custom_fields
20 helper :custom_fields
21 include CustomFieldsHelper
21 include CustomFieldsHelper
22
22
23 # prevents login action to be filtered by check_if_login_required application scope filter
23 # prevents login action to be filtered by check_if_login_required application scope filter
24 skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
24 skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
25
25
26 # Show user's account
26 # Show user's account
27 def show
27 def show
28 @user = User.find_active(params[:id])
28 @user = User.find_active(params[:id])
29 @custom_values = @user.custom_values.find(:all, :include => :custom_field)
29 @custom_values = @user.custom_values.find(:all, :include => :custom_field)
30
30
31 # show only public projects and private projects that the logged in user is also a member of
31 # show only public projects and private projects that the logged in user is also a member of
32 @memberships = @user.memberships.select do |membership|
32 @memberships = @user.memberships.select do |membership|
33 membership.project.is_public? || (User.current.member_of?(membership.project))
33 membership.project.is_public? || (User.current.member_of?(membership.project))
34 end
34 end
35 rescue ActiveRecord::RecordNotFound
35 rescue ActiveRecord::RecordNotFound
36 render_404
36 render_404
37 end
37 end
38
38
39 # Login request and validation
39 # Login request and validation
40 def login
40 def login
41 if request.get?
41 if request.get?
42 # Logout user
42 # Logout user
43 self.logged_user = nil
43 self.logged_user = nil
44 else
44 else
45 # Authenticate user
45 # Authenticate user
46 user = User.try_to_login(params[:username], params[:password])
46 user = User.try_to_login(params[:username], params[:password])
47 if user
47 if user
48 self.logged_user = user
48 self.logged_user = user
49 # generate a key and set cookie if autologin
49 # generate a key and set cookie if autologin
50 if params[:autologin] && Setting.autologin?
50 if params[:autologin] && Setting.autologin?
51 token = Token.create(:user => user, :action => 'autologin')
51 token = Token.create(:user => user, :action => 'autologin')
52 cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
52 cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
53 end
53 end
54 redirect_back_or_default :controller => 'my', :action => 'page'
54 redirect_back_or_default :controller => 'my', :action => 'page'
55 else
55 else
56 flash.now[:error] = l(:notice_account_invalid_creditentials)
56 flash.now[:error] = l(:notice_account_invalid_creditentials)
57 end
57 end
58 end
58 end
59 rescue User::OnTheFlyCreationFailure
59 rescue User::OnTheFlyCreationFailure
60 flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
60 flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
61 end
61 end
62
62
63 # Log out current user and redirect to welcome page
63 # Log out current user and redirect to welcome page
64 def logout
64 def logout
65 cookies.delete :autologin
65 cookies.delete :autologin
66 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
66 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
67 self.logged_user = nil
67 self.logged_user = nil
68 redirect_to home_url
68 redirect_to home_url
69 end
69 end
70
70
71 # Enable user to choose a new password
71 # Enable user to choose a new password
72 def lost_password
72 def lost_password
73 redirect_to(home_url) && return unless Setting.lost_password?
73 redirect_to(home_url) && return unless Setting.lost_password?
74 if params[:token]
74 if params[:token]
75 @token = Token.find_by_action_and_value("recovery", params[:token])
75 @token = Token.find_by_action_and_value("recovery", params[:token])
76 redirect_to(home_url) && return unless @token and !@token.expired?
76 redirect_to(home_url) && return unless @token and !@token.expired?
77 @user = @token.user
77 @user = @token.user
78 if request.post?
78 if request.post?
79 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
79 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
80 if @user.save
80 if @user.save
81 @token.destroy
81 @token.destroy
82 flash[:notice] = l(:notice_account_password_updated)
82 flash[:notice] = l(:notice_account_password_updated)
83 redirect_to :action => 'login'
83 redirect_to :action => 'login'
84 return
84 return
85 end
85 end
86 end
86 end
87 render :template => "account/password_recovery"
87 render :template => "account/password_recovery"
88 return
88 return
89 else
89 else
90 if request.post?
90 if request.post?
91 user = User.find_by_mail(params[:mail])
91 user = User.find_by_mail(params[:mail])
92 # user not found in db
92 # user not found in db
93 flash.now[:error] = l(:notice_account_unknown_email) and return unless user
93 flash.now[:error] = l(:notice_account_unknown_email) and return unless user
94 # user uses an external authentification
94 # user uses an external authentification
95 flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
95 flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
96 # create a new token for password recovery
96 # create a new token for password recovery
97 token = Token.new(:user => user, :action => "recovery")
97 token = Token.new(:user => user, :action => "recovery")
98 if token.save
98 if token.save
99 Mailer.deliver_lost_password(token)
99 Mailer.deliver_lost_password(token)
100 flash[:notice] = l(:notice_account_lost_email_sent)
100 flash[:notice] = l(:notice_account_lost_email_sent)
101 redirect_to :action => 'login'
101 redirect_to :action => 'login'
102 return
102 return
103 end
103 end
104 end
104 end
105 end
105 end
106 end
106 end
107
107
108 # User self-registration
108 # User self-registration
109 def register
109 def register
110 redirect_to(home_url) && return unless Setting.self_registration?
110 redirect_to(home_url) && return unless Setting.self_registration?
111 if request.get?
111 if request.get?
112 @user = User.new(:language => Setting.default_language)
112 @user = User.new(:language => Setting.default_language)
113 @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
114 else
113 else
115 @user = User.new(params[:user])
114 @user = User.new(params[:user])
116 @user.admin = false
115 @user.admin = false
117 @user.login = params[:user][:login]
116 @user.login = params[:user][:login]
118 @user.status = User::STATUS_REGISTERED
117 @user.status = User::STATUS_REGISTERED
119 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
118 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
120 @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x,
121 :customized => @user,
122 :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) }
123 @user.custom_values = @custom_values
124 case Setting.self_registration
119 case Setting.self_registration
125 when '1'
120 when '1'
126 # Email activation
121 # Email activation
127 token = Token.new(:user => @user, :action => "register")
122 token = Token.new(:user => @user, :action => "register")
128 if @user.save and token.save
123 if @user.save and token.save
129 Mailer.deliver_register(token)
124 Mailer.deliver_register(token)
130 flash[:notice] = l(:notice_account_register_done)
125 flash[:notice] = l(:notice_account_register_done)
131 redirect_to :action => 'login'
126 redirect_to :action => 'login'
132 end
127 end
133 when '3'
128 when '3'
134 # Automatic activation
129 # Automatic activation
135 @user.status = User::STATUS_ACTIVE
130 @user.status = User::STATUS_ACTIVE
136 if @user.save
131 if @user.save
137 self.logged_user = @user
132 self.logged_user = @user
138 flash[:notice] = l(:notice_account_activated)
133 flash[:notice] = l(:notice_account_activated)
139 redirect_to :controller => 'my', :action => 'account'
134 redirect_to :controller => 'my', :action => 'account'
140 end
135 end
141 else
136 else
142 # Manual activation by the administrator
137 # Manual activation by the administrator
143 if @user.save
138 if @user.save
144 # Sends an email to the administrators
139 # Sends an email to the administrators
145 Mailer.deliver_account_activation_request(@user)
140 Mailer.deliver_account_activation_request(@user)
146 flash[:notice] = l(:notice_account_pending)
141 flash[:notice] = l(:notice_account_pending)
147 redirect_to :action => 'login'
142 redirect_to :action => 'login'
148 end
143 end
149 end
144 end
150 end
145 end
151 end
146 end
152
147
153 # Token based account activation
148 # Token based account activation
154 def activate
149 def activate
155 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
150 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
156 token = Token.find_by_action_and_value('register', params[:token])
151 token = Token.find_by_action_and_value('register', params[:token])
157 redirect_to(home_url) && return unless token and !token.expired?
152 redirect_to(home_url) && return unless token and !token.expired?
158 user = token.user
153 user = token.user
159 redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
154 redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
160 user.status = User::STATUS_ACTIVE
155 user.status = User::STATUS_ACTIVE
161 if user.save
156 if user.save
162 token.destroy
157 token.destroy
163 flash[:notice] = l(:notice_account_activated)
158 flash[:notice] = l(:notice_account_activated)
164 end
159 end
165 redirect_to :action => 'login'
160 redirect_to :action => 'login'
166 end
161 end
167
162
168 private
163 private
169 def logged_user=(user)
164 def logged_user=(user)
170 if user && user.is_a?(User)
165 if user && user.is_a?(User)
171 User.current = user
166 User.current = user
172 session[:user_id] = user.id
167 session[:user_id] = user.id
173 else
168 else
174 User.current = User.anonymous
169 User.current = User.anonymous
175 session[:user_id] = nil
170 session[:user_id] = nil
176 end
171 end
177 end
172 end
178 end
173 end
@@ -1,442 +1,428
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :new_issue, :only => :new
20 menu_item :new_issue, :only => :new
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
22 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes]
26 before_filter :find_optional_project, :only => [:index, :changes]
27 accept_key_auth :index, :changes
27 accept_key_auth :index, :changes
28
28
29 helper :journals
29 helper :journals
30 helper :projects
30 helper :projects
31 include ProjectsHelper
31 include ProjectsHelper
32 helper :custom_fields
32 helper :custom_fields
33 include CustomFieldsHelper
33 include CustomFieldsHelper
34 helper :ifpdf
34 helper :ifpdf
35 include IfpdfHelper
35 include IfpdfHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 helper :sort
43 helper :sort
44 include SortHelper
44 include SortHelper
45 include IssuesHelper
45 include IssuesHelper
46
46
47 def index
47 def index
48 sort_init "#{Issue.table_name}.id", "desc"
48 sort_init "#{Issue.table_name}.id", "desc"
49 sort_update
49 sort_update
50 retrieve_query
50 retrieve_query
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 sort_init "#{Issue.table_name}.id", "desc"
81 sort_init "#{Issue.table_name}.id", "desc"
82 sort_update
82 sort_update
83 retrieve_query
83 retrieve_query
84 if @query.valid?
84 if @query.valid?
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 :conditions => @query.statement,
86 :conditions => @query.statement,
87 :limit => 25,
87 :limit => 25,
88 :order => "#{Journal.table_name}.created_on DESC"
88 :order => "#{Journal.table_name}.created_on DESC"
89 end
89 end
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 render :layout => false, :content_type => 'application/atom+xml'
91 render :layout => false, :content_type => 'application/atom+xml'
92 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
93 render_404
93 render_404
94 end
94 end
95
95
96 def show
96 def show
97 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals.each_with_index {|j,i| j.indice = i+1}
98 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @activities = Enumeration::get_values('ACTI')
102 @activities = Enumeration::get_values('ACTI')
104 @priorities = Enumeration::get_values('IPRI')
103 @priorities = Enumeration::get_values('IPRI')
105 @time_entry = TimeEntry.new
104 @time_entry = TimeEntry.new
106 respond_to do |format|
105 respond_to do |format|
107 format.html { render :template => 'issues/show.rhtml' }
106 format.html { render :template => 'issues/show.rhtml' }
108 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
109 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
110 end
109 end
111 end
110 end
112
111
113 # Add a new issue
112 # Add a new issue
114 # The new issue will be created from an existing one if copy_from parameter is given
113 # The new issue will be created from an existing one if copy_from parameter is given
115 def new
114 def new
116 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
115 @issue = Issue.new
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 @issue.project = @project
117 @issue.project = @project
118 @issue.author = User.current
119 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
118 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
120 if @issue.tracker.nil?
119 if @issue.tracker.nil?
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 render :nothing => true, :layout => true
121 render :nothing => true, :layout => true
123 return
122 return
124 end
123 end
124 @issue.attributes = params[:issue]
125 @issue.author = User.current
125
126
126 default_status = IssueStatus.default
127 default_status = IssueStatus.default
127 unless default_status
128 unless default_status
128 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
129 render :nothing => true, :layout => true
130 render :nothing => true, :layout => true
130 return
131 return
131 end
132 end
132 @issue.status = default_status
133 @issue.status = default_status
133 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
134
135
135 if request.get? || request.xhr?
136 if request.get? || request.xhr?
136 @issue.start_date ||= Date.today
137 @issue.start_date ||= Date.today
137 @custom_values = @issue.custom_values.empty? ?
138 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
139 @issue.custom_values
140 else
138 else
141 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
142 # Check that the user is allowed to apply the requested status
140 # Check that the user is allowed to apply the requested status
143 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
144 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
145 :customized => @issue,
146 :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
147 @issue.custom_values = @custom_values
148 if @issue.save
142 if @issue.save
149 attach_files(@issue, params[:attachments])
143 attach_files(@issue, params[:attachments])
150 flash[:notice] = l(:notice_successful_create)
144 flash[:notice] = l(:notice_successful_create)
151 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
152 redirect_to :controller => 'issues', :action => 'show', :id => @issue
146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
153 return
147 return
154 end
148 end
155 end
149 end
156 @priorities = Enumeration::get_values('IPRI')
150 @priorities = Enumeration::get_values('IPRI')
157 render :layout => !request.xhr?
151 render :layout => !request.xhr?
158 end
152 end
159
153
160 # Attributes that can be updated on workflow transition (without :edit permission)
154 # Attributes that can be updated on workflow transition (without :edit permission)
161 # TODO: make it configurable (at least per role)
155 # TODO: make it configurable (at least per role)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163
157
164 def edit
158 def edit
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @activities = Enumeration::get_values('ACTI')
160 @activities = Enumeration::get_values('ACTI')
167 @priorities = Enumeration::get_values('IPRI')
161 @priorities = Enumeration::get_values('IPRI')
168 @custom_values = []
169 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
170
163
171 @notes = params[:notes]
164 @notes = params[:notes]
172 journal = @issue.init_journal(User.current, @notes)
165 journal = @issue.init_journal(User.current, @notes)
173 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
174 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
175 attrs = params[:issue].dup
168 attrs = params[:issue].dup
176 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
177 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
178 @issue.attributes = attrs
171 @issue.attributes = attrs
179 end
172 end
180
173
181 if request.get?
174 if request.post?
182 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
183 else
184 # Update custom fields if user has :edit permission
185 if @edit_allowed && params[:custom_fields]
186 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
187 @issue.custom_values = @custom_values
188 end
189 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
190 @time_entry.attributes = params[:time_entry]
176 @time_entry.attributes = params[:time_entry]
191 attachments = attach_files(@issue, params[:attachments])
177 attachments = attach_files(@issue, params[:attachments])
192 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
193 if @issue.save
179 if @issue.save
194 # Log spend time
180 # Log spend time
195 if current_role.allowed_to?(:log_time)
181 if current_role.allowed_to?(:log_time)
196 @time_entry.save
182 @time_entry.save
197 end
183 end
198 if !journal.new_record?
184 if !journal.new_record?
199 # Only send notification if something was actually changed
185 # Only send notification if something was actually changed
200 flash[:notice] = l(:notice_successful_update)
186 flash[:notice] = l(:notice_successful_update)
201 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
187 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
202 end
188 end
203 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
189 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
204 end
190 end
205 end
191 end
206 rescue ActiveRecord::StaleObjectError
192 rescue ActiveRecord::StaleObjectError
207 # Optimistic locking exception
193 # Optimistic locking exception
208 flash.now[:error] = l(:notice_locking_conflict)
194 flash.now[:error] = l(:notice_locking_conflict)
209 end
195 end
210
196
211 def reply
197 def reply
212 journal = Journal.find(params[:journal_id]) if params[:journal_id]
198 journal = Journal.find(params[:journal_id]) if params[:journal_id]
213 if journal
199 if journal
214 user = journal.user
200 user = journal.user
215 text = journal.notes
201 text = journal.notes
216 else
202 else
217 user = @issue.author
203 user = @issue.author
218 text = @issue.description
204 text = @issue.description
219 end
205 end
220 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
206 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
221 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
207 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
222 render(:update) { |page|
208 render(:update) { |page|
223 page.<< "$('notes').value = \"#{content}\";"
209 page.<< "$('notes').value = \"#{content}\";"
224 page.show 'update'
210 page.show 'update'
225 page << "Form.Element.focus('notes');"
211 page << "Form.Element.focus('notes');"
226 page << "Element.scrollTo('update');"
212 page << "Element.scrollTo('update');"
227 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
213 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
228 }
214 }
229 end
215 end
230
216
231 # Bulk edit a set of issues
217 # Bulk edit a set of issues
232 def bulk_edit
218 def bulk_edit
233 if request.post?
219 if request.post?
234 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
220 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
235 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
221 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
236 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
222 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
237 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
223 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
238 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
224 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
239
225
240 unsaved_issue_ids = []
226 unsaved_issue_ids = []
241 @issues.each do |issue|
227 @issues.each do |issue|
242 journal = issue.init_journal(User.current, params[:notes])
228 journal = issue.init_journal(User.current, params[:notes])
243 issue.priority = priority if priority
229 issue.priority = priority if priority
244 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
230 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
245 issue.category = category if category || params[:category_id] == 'none'
231 issue.category = category if category || params[:category_id] == 'none'
246 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
232 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
247 issue.start_date = params[:start_date] unless params[:start_date].blank?
233 issue.start_date = params[:start_date] unless params[:start_date].blank?
248 issue.due_date = params[:due_date] unless params[:due_date].blank?
234 issue.due_date = params[:due_date] unless params[:due_date].blank?
249 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
235 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
250 # Don't save any change to the issue if the user is not authorized to apply the requested status
236 # Don't save any change to the issue if the user is not authorized to apply the requested status
251 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
237 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
252 # Send notification for each issue (if changed)
238 # Send notification for each issue (if changed)
253 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
239 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
254 else
240 else
255 # Keep unsaved issue ids to display them in flash error
241 # Keep unsaved issue ids to display them in flash error
256 unsaved_issue_ids << issue.id
242 unsaved_issue_ids << issue.id
257 end
243 end
258 end
244 end
259 if unsaved_issue_ids.empty?
245 if unsaved_issue_ids.empty?
260 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
246 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
261 else
247 else
262 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
248 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
263 end
249 end
264 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
250 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
265 return
251 return
266 end
252 end
267 # Find potential statuses the user could be allowed to switch issues to
253 # Find potential statuses the user could be allowed to switch issues to
268 @available_statuses = Workflow.find(:all, :include => :new_status,
254 @available_statuses = Workflow.find(:all, :include => :new_status,
269 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
255 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
270 end
256 end
271
257
272 def move
258 def move
273 @allowed_projects = []
259 @allowed_projects = []
274 # find projects to which the user is allowed to move the issue
260 # find projects to which the user is allowed to move the issue
275 if User.current.admin?
261 if User.current.admin?
276 # admin is allowed to move issues to any active (visible) project
262 # admin is allowed to move issues to any active (visible) project
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
263 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
278 else
264 else
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
265 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
280 end
266 end
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
267 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
282 @target_project ||= @project
268 @target_project ||= @project
283 @trackers = @target_project.trackers
269 @trackers = @target_project.trackers
284 if request.post?
270 if request.post?
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
271 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 unsaved_issue_ids = []
272 unsaved_issue_ids = []
287 @issues.each do |issue|
273 @issues.each do |issue|
288 issue.init_journal(User.current)
274 issue.init_journal(User.current)
289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
275 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
290 end
276 end
291 if unsaved_issue_ids.empty?
277 if unsaved_issue_ids.empty?
292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
278 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
293 else
279 else
294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
280 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
295 end
281 end
296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
282 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
297 return
283 return
298 end
284 end
299 render :layout => false if request.xhr?
285 render :layout => false if request.xhr?
300 end
286 end
301
287
302 def destroy
288 def destroy
303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
289 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
304 if @hours > 0
290 if @hours > 0
305 case params[:todo]
291 case params[:todo]
306 when 'destroy'
292 when 'destroy'
307 # nothing to do
293 # nothing to do
308 when 'nullify'
294 when 'nullify'
309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
295 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
310 when 'reassign'
296 when 'reassign'
311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
297 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
312 if reassign_to.nil?
298 if reassign_to.nil?
313 flash.now[:error] = l(:error_issue_not_found_in_project)
299 flash.now[:error] = l(:error_issue_not_found_in_project)
314 return
300 return
315 else
301 else
316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
302 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
317 end
303 end
318 else
304 else
319 # display the destroy form
305 # display the destroy form
320 return
306 return
321 end
307 end
322 end
308 end
323 @issues.each(&:destroy)
309 @issues.each(&:destroy)
324 redirect_to :action => 'index', :project_id => @project
310 redirect_to :action => 'index', :project_id => @project
325 end
311 end
326
312
327 def destroy_attachment
313 def destroy_attachment
328 a = @issue.attachments.find(params[:attachment_id])
314 a = @issue.attachments.find(params[:attachment_id])
329 a.destroy
315 a.destroy
330 journal = @issue.init_journal(User.current)
316 journal = @issue.init_journal(User.current)
331 journal.details << JournalDetail.new(:property => 'attachment',
317 journal.details << JournalDetail.new(:property => 'attachment',
332 :prop_key => a.id,
318 :prop_key => a.id,
333 :old_value => a.filename)
319 :old_value => a.filename)
334 journal.save
320 journal.save
335 redirect_to :action => 'show', :id => @issue
321 redirect_to :action => 'show', :id => @issue
336 end
322 end
337
323
338 def context_menu
324 def context_menu
339 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
325 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
340 if (@issues.size == 1)
326 if (@issues.size == 1)
341 @issue = @issues.first
327 @issue = @issues.first
342 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
328 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
343 @assignables = @issue.assignable_users
329 @assignables = @issue.assignable_users
344 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
330 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
345 end
331 end
346 projects = @issues.collect(&:project).compact.uniq
332 projects = @issues.collect(&:project).compact.uniq
347 @project = projects.first if projects.size == 1
333 @project = projects.first if projects.size == 1
348
334
349 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
335 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
350 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
336 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
351 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
337 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
352 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
338 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
353 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
339 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
354 }
340 }
355
341
356 @priorities = Enumeration.get_values('IPRI').reverse
342 @priorities = Enumeration.get_values('IPRI').reverse
357 @statuses = IssueStatus.find(:all, :order => 'position')
343 @statuses = IssueStatus.find(:all, :order => 'position')
358 @back = request.env['HTTP_REFERER']
344 @back = request.env['HTTP_REFERER']
359
345
360 render :layout => false
346 render :layout => false
361 end
347 end
362
348
363 def update_form
349 def update_form
364 @issue = Issue.new(params[:issue])
350 @issue = Issue.new(params[:issue])
365 render :action => :new, :layout => false
351 render :action => :new, :layout => false
366 end
352 end
367
353
368 def preview
354 def preview
369 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
355 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
370 @attachements = @issue.attachments if @issue
356 @attachements = @issue.attachments if @issue
371 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
357 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
372 render :partial => 'common/preview'
358 render :partial => 'common/preview'
373 end
359 end
374
360
375 private
361 private
376 def find_issue
362 def find_issue
377 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
363 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
378 @project = @issue.project
364 @project = @issue.project
379 rescue ActiveRecord::RecordNotFound
365 rescue ActiveRecord::RecordNotFound
380 render_404
366 render_404
381 end
367 end
382
368
383 # Filter for bulk operations
369 # Filter for bulk operations
384 def find_issues
370 def find_issues
385 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
371 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
386 raise ActiveRecord::RecordNotFound if @issues.empty?
372 raise ActiveRecord::RecordNotFound if @issues.empty?
387 projects = @issues.collect(&:project).compact.uniq
373 projects = @issues.collect(&:project).compact.uniq
388 if projects.size == 1
374 if projects.size == 1
389 @project = projects.first
375 @project = projects.first
390 else
376 else
391 # TODO: let users bulk edit/move/destroy issues from different projects
377 # TODO: let users bulk edit/move/destroy issues from different projects
392 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
378 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
393 end
379 end
394 rescue ActiveRecord::RecordNotFound
380 rescue ActiveRecord::RecordNotFound
395 render_404
381 render_404
396 end
382 end
397
383
398 def find_project
384 def find_project
399 @project = Project.find(params[:project_id])
385 @project = Project.find(params[:project_id])
400 rescue ActiveRecord::RecordNotFound
386 rescue ActiveRecord::RecordNotFound
401 render_404
387 render_404
402 end
388 end
403
389
404 def find_optional_project
390 def find_optional_project
405 return true unless params[:project_id]
391 return true unless params[:project_id]
406 @project = Project.find(params[:project_id])
392 @project = Project.find(params[:project_id])
407 authorize
393 authorize
408 rescue ActiveRecord::RecordNotFound
394 rescue ActiveRecord::RecordNotFound
409 render_404
395 render_404
410 end
396 end
411
397
412 # Retrieve query from session or build a new query
398 # Retrieve query from session or build a new query
413 def retrieve_query
399 def retrieve_query
414 if !params[:query_id].blank?
400 if !params[:query_id].blank?
415 cond = "project_id IS NULL"
401 cond = "project_id IS NULL"
416 cond << " OR project_id = #{@project.id}" if @project
402 cond << " OR project_id = #{@project.id}" if @project
417 @query = Query.find(params[:query_id], :conditions => cond)
403 @query = Query.find(params[:query_id], :conditions => cond)
418 @query.project = @project
404 @query.project = @project
419 session[:query] = {:id => @query.id, :project_id => @query.project_id}
405 session[:query] = {:id => @query.id, :project_id => @query.project_id}
420 else
406 else
421 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
407 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
422 # Give it a name, required to be valid
408 # Give it a name, required to be valid
423 @query = Query.new(:name => "_")
409 @query = Query.new(:name => "_")
424 @query.project = @project
410 @query.project = @project
425 if params[:fields] and params[:fields].is_a? Array
411 if params[:fields] and params[:fields].is_a? Array
426 params[:fields].each do |field|
412 params[:fields].each do |field|
427 @query.add_filter(field, params[:operators][field], params[:values][field])
413 @query.add_filter(field, params[:operators][field], params[:values][field])
428 end
414 end
429 else
415 else
430 @query.available_filters.keys.each do |field|
416 @query.available_filters.keys.each do |field|
431 @query.add_short_filter(field, params[field]) if params[field]
417 @query.add_short_filter(field, params[field]) if params[field]
432 end
418 end
433 end
419 end
434 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
420 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
435 else
421 else
436 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
422 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
437 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
423 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
438 @query.project = @project
424 @query.project = @project
439 end
425 end
440 end
426 end
441 end
427 end
442 end
428 end
@@ -1,444 +1,434
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :overview
20 menu_item :overview
21 menu_item :activity, :only => :activity
21 menu_item :activity, :only => :activity
22 menu_item :roadmap, :only => :roadmap
22 menu_item :roadmap, :only => :roadmap
23 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :files, :only => [:list_files, :add_file]
24 menu_item :settings, :only => :settings
24 menu_item :settings, :only => :settings
25 menu_item :issues, :only => [:changelog]
25 menu_item :issues, :only => [:changelog]
26
26
27 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
28 before_filter :find_optional_project, :only => :activity
28 before_filter :find_optional_project, :only => :activity
29 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
30 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
31 accept_key_auth :activity, :calendar
31 accept_key_auth :activity, :calendar
32
32
33 helper :sort
33 helper :sort
34 include SortHelper
34 include SortHelper
35 helper :custom_fields
35 helper :custom_fields
36 include CustomFieldsHelper
36 include CustomFieldsHelper
37 helper :ifpdf
37 helper :ifpdf
38 include IfpdfHelper
38 include IfpdfHelper
39 helper :issues
39 helper :issues
40 helper IssuesHelper
40 helper IssuesHelper
41 helper :queries
41 helper :queries
42 include QueriesHelper
42 include QueriesHelper
43 helper :repositories
43 helper :repositories
44 include RepositoriesHelper
44 include RepositoriesHelper
45 include ProjectsHelper
45 include ProjectsHelper
46
46
47 # Lists visible projects
47 # Lists visible projects
48 def index
48 def index
49 projects = Project.find :all,
49 projects = Project.find :all,
50 :conditions => Project.visible_by(User.current),
50 :conditions => Project.visible_by(User.current),
51 :include => :parent
51 :include => :parent
52 respond_to do |format|
52 respond_to do |format|
53 format.html {
53 format.html {
54 @project_tree = projects.group_by {|p| p.parent || p}
54 @project_tree = projects.group_by {|p| p.parent || p}
55 @project_tree.each_key {|p| @project_tree[p] -= [p]}
55 @project_tree.each_key {|p| @project_tree[p] -= [p]}
56 }
56 }
57 format.atom {
57 format.atom {
58 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
59 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 # Add a new project
64 # Add a new project
65 def add
65 def add
66 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @trackers = Tracker.all
67 @trackers = Tracker.all
68 @root_projects = Project.find(:all,
68 @root_projects = Project.find(:all,
69 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
70 :order => 'name')
70 :order => 'name')
71 @project = Project.new(params[:project])
71 @project = Project.new(params[:project])
72 if request.get?
72 if request.get?
73 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
74 @project.trackers = Tracker.all
73 @project.trackers = Tracker.all
75 @project.is_public = Setting.default_projects_public?
74 @project.is_public = Setting.default_projects_public?
76 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
77 else
76 else
78 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
79 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
80 @project.custom_values = @custom_values
81 @project.enabled_module_names = params[:enabled_modules]
77 @project.enabled_module_names = params[:enabled_modules]
82 if @project.save
78 if @project.save
83 flash[:notice] = l(:notice_successful_create)
79 flash[:notice] = l(:notice_successful_create)
84 redirect_to :controller => 'admin', :action => 'projects'
80 redirect_to :controller => 'admin', :action => 'projects'
85 end
81 end
86 end
82 end
87 end
83 end
88
84
89 # Show @project
85 # Show @project
90 def show
86 def show
91 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
92 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
93 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
94 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
95 @trackers = @project.rolled_up_trackers
90 @trackers = @project.rolled_up_trackers
96
91
97 cond = @project.project_condition(Setting.display_subprojects_issues?)
92 cond = @project.project_condition(Setting.display_subprojects_issues?)
98 Issue.visible_by(User.current) do
93 Issue.visible_by(User.current) do
99 @open_issues_by_tracker = Issue.count(:group => :tracker,
94 @open_issues_by_tracker = Issue.count(:group => :tracker,
100 :include => [:project, :status, :tracker],
95 :include => [:project, :status, :tracker],
101 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
102 @total_issues_by_tracker = Issue.count(:group => :tracker,
97 @total_issues_by_tracker = Issue.count(:group => :tracker,
103 :include => [:project, :status, :tracker],
98 :include => [:project, :status, :tracker],
104 :conditions => cond)
99 :conditions => cond)
105 end
100 end
106 TimeEntry.visible_by(User.current) do
101 TimeEntry.visible_by(User.current) do
107 @total_hours = TimeEntry.sum(:hours,
102 @total_hours = TimeEntry.sum(:hours,
108 :include => :project,
103 :include => :project,
109 :conditions => cond).to_f
104 :conditions => cond).to_f
110 end
105 end
111 @key = User.current.rss_key
106 @key = User.current.rss_key
112 end
107 end
113
108
114 def settings
109 def settings
115 @root_projects = Project.find(:all,
110 @root_projects = Project.find(:all,
116 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
117 :order => 'name')
112 :order => 'name')
118 @custom_fields = IssueCustomField.find(:all)
113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
119 @issue_category ||= IssueCategory.new
114 @issue_category ||= IssueCategory.new
120 @member ||= @project.members.new
115 @member ||= @project.members.new
121 @trackers = Tracker.all
116 @trackers = Tracker.all
122 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
123 @repository ||= @project.repository
117 @repository ||= @project.repository
124 @wiki ||= @project.wiki
118 @wiki ||= @project.wiki
125 end
119 end
126
120
127 # Edit @project
121 # Edit @project
128 def edit
122 def edit
129 if request.post?
123 if request.post?
130 if params[:custom_fields]
131 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
132 @project.custom_values = @custom_values
133 end
134 @project.attributes = params[:project]
124 @project.attributes = params[:project]
135 if @project.save
125 if @project.save
136 flash[:notice] = l(:notice_successful_update)
126 flash[:notice] = l(:notice_successful_update)
137 redirect_to :action => 'settings', :id => @project
127 redirect_to :action => 'settings', :id => @project
138 else
128 else
139 settings
129 settings
140 render :action => 'settings'
130 render :action => 'settings'
141 end
131 end
142 end
132 end
143 end
133 end
144
134
145 def modules
135 def modules
146 @project.enabled_module_names = params[:enabled_modules]
136 @project.enabled_module_names = params[:enabled_modules]
147 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
148 end
138 end
149
139
150 def archive
140 def archive
151 @project.archive if request.post? && @project.active?
141 @project.archive if request.post? && @project.active?
152 redirect_to :controller => 'admin', :action => 'projects'
142 redirect_to :controller => 'admin', :action => 'projects'
153 end
143 end
154
144
155 def unarchive
145 def unarchive
156 @project.unarchive if request.post? && !@project.active?
146 @project.unarchive if request.post? && !@project.active?
157 redirect_to :controller => 'admin', :action => 'projects'
147 redirect_to :controller => 'admin', :action => 'projects'
158 end
148 end
159
149
160 # Delete @project
150 # Delete @project
161 def destroy
151 def destroy
162 @project_to_destroy = @project
152 @project_to_destroy = @project
163 if request.post? and params[:confirm]
153 if request.post? and params[:confirm]
164 @project_to_destroy.destroy
154 @project_to_destroy.destroy
165 redirect_to :controller => 'admin', :action => 'projects'
155 redirect_to :controller => 'admin', :action => 'projects'
166 end
156 end
167 # hide project in layout
157 # hide project in layout
168 @project = nil
158 @project = nil
169 end
159 end
170
160
171 # Add a new issue category to @project
161 # Add a new issue category to @project
172 def add_issue_category
162 def add_issue_category
173 @category = @project.issue_categories.build(params[:category])
163 @category = @project.issue_categories.build(params[:category])
174 if request.post? and @category.save
164 if request.post? and @category.save
175 respond_to do |format|
165 respond_to do |format|
176 format.html do
166 format.html do
177 flash[:notice] = l(:notice_successful_create)
167 flash[:notice] = l(:notice_successful_create)
178 redirect_to :action => 'settings', :tab => 'categories', :id => @project
168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
179 end
169 end
180 format.js do
170 format.js do
181 # IE doesn't support the replace_html rjs method for select box options
171 # IE doesn't support the replace_html rjs method for select box options
182 render(:update) {|page| page.replace "issue_category_id",
172 render(:update) {|page| page.replace "issue_category_id",
183 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]')
173 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]')
184 }
174 }
185 end
175 end
186 end
176 end
187 end
177 end
188 end
178 end
189
179
190 # Add a new version to @project
180 # Add a new version to @project
191 def add_version
181 def add_version
192 @version = @project.versions.build(params[:version])
182 @version = @project.versions.build(params[:version])
193 if request.post? and @version.save
183 if request.post? and @version.save
194 flash[:notice] = l(:notice_successful_create)
184 flash[:notice] = l(:notice_successful_create)
195 redirect_to :action => 'settings', :tab => 'versions', :id => @project
185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
196 end
186 end
197 end
187 end
198
188
199 def add_file
189 def add_file
200 if request.post?
190 if request.post?
201 @version = @project.versions.find_by_id(params[:version_id])
191 @version = @project.versions.find_by_id(params[:version_id])
202 attachments = attach_files(@version, params[:attachments])
192 attachments = attach_files(@version, params[:attachments])
203 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
204 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
205 end
195 end
206 @versions = @project.versions.sort
196 @versions = @project.versions.sort
207 end
197 end
208
198
209 def list_files
199 def list_files
210 sort_init "#{Attachment.table_name}.filename", "asc"
200 sort_init "#{Attachment.table_name}.filename", "asc"
211 sort_update
201 sort_update
212 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
202 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
213 render :layout => !request.xhr?
203 render :layout => !request.xhr?
214 end
204 end
215
205
216 # Show changelog for @project
206 # Show changelog for @project
217 def changelog
207 def changelog
218 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
208 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
219 retrieve_selected_tracker_ids(@trackers)
209 retrieve_selected_tracker_ids(@trackers)
220 @versions = @project.versions.sort
210 @versions = @project.versions.sort
221 end
211 end
222
212
223 def roadmap
213 def roadmap
224 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
214 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
225 retrieve_selected_tracker_ids(@trackers)
215 retrieve_selected_tracker_ids(@trackers)
226 @versions = @project.versions.sort
216 @versions = @project.versions.sort
227 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
217 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
228 end
218 end
229
219
230 def activity
220 def activity
231 @days = Setting.activity_days_default.to_i
221 @days = Setting.activity_days_default.to_i
232
222
233 if params[:from]
223 if params[:from]
234 begin; @date_to = params[:from].to_date; rescue; end
224 begin; @date_to = params[:from].to_date; rescue; end
235 end
225 end
236
226
237 @date_to ||= Date.today + 1
227 @date_to ||= Date.today + 1
238 @date_from = @date_to - @days
228 @date_from = @date_to - @days
239
229
240 @event_types = %w(issues news files documents changesets wiki_pages messages)
230 @event_types = %w(issues news files documents changesets wiki_pages messages)
241 if @project
231 if @project
242 @event_types.delete('wiki_pages') unless @project.wiki
232 @event_types.delete('wiki_pages') unless @project.wiki
243 @event_types.delete('changesets') unless @project.repository
233 @event_types.delete('changesets') unless @project.repository
244 @event_types.delete('messages') unless @project.boards.any?
234 @event_types.delete('messages') unless @project.boards.any?
245 # only show what the user is allowed to view
235 # only show what the user is allowed to view
246 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
236 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
247 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
237 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
248 end
238 end
249 @scope = @event_types.select {|t| params["show_#{t}"]}
239 @scope = @event_types.select {|t| params["show_#{t}"]}
250 # default events if none is specified in parameters
240 # default events if none is specified in parameters
251 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
241 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
252
242
253 @events = []
243 @events = []
254
244
255 if @scope.include?('issues')
245 if @scope.include?('issues')
256 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
246 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
257 cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
247 cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
258 @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions)
248 @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions)
259
249
260 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
250 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
261 cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
251 cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
262 cond.add("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''")
252 cond.add("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''")
263 @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions)
253 @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions)
264 end
254 end
265
255
266 if @scope.include?('news')
256 if @scope.include?('news')
267 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects))
257 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects))
268 cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
258 cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
269 @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions)
259 @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions)
270 end
260 end
271
261
272 if @scope.include?('files')
262 if @scope.include?('files')
273 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects))
263 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects))
274 cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
264 cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
275 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
265 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
276 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
266 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
277 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id",
267 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id",
278 :conditions => cond.conditions)
268 :conditions => cond.conditions)
279 end
269 end
280
270
281 if @scope.include?('documents')
271 if @scope.include?('documents')
282 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
272 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
283 cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
273 cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
284 @events += Document.find(:all, :include => :project, :conditions => cond.conditions)
274 @events += Document.find(:all, :include => :project, :conditions => cond.conditions)
285
275
286 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
276 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
287 cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
277 cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
288 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
278 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
289 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
279 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
290 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id",
280 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id",
291 :conditions => cond.conditions)
281 :conditions => cond.conditions)
292 end
282 end
293
283
294 if @scope.include?('wiki_pages')
284 if @scope.include?('wiki_pages')
295 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
285 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
296 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
286 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
297 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
287 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
298 "#{WikiContent.versioned_table_name}.id"
288 "#{WikiContent.versioned_table_name}.id"
299 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
289 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
300 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
290 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
301 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"
291 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"
302
292
303 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects))
293 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects))
304 cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to])
294 cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to])
305 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions)
295 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions)
306 end
296 end
307
297
308 if @scope.include?('changesets')
298 if @scope.include?('changesets')
309 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects))
299 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects))
310 cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
300 cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
311 @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions)
301 @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions)
312 end
302 end
313
303
314 if @scope.include?('messages')
304 if @scope.include?('messages')
315 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects))
305 cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects))
316 cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
306 cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
317 @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions)
307 @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions)
318 end
308 end
319
309
320 @events_by_day = @events.group_by(&:event_date)
310 @events_by_day = @events.group_by(&:event_date)
321
311
322 respond_to do |format|
312 respond_to do |format|
323 format.html { render :layout => false if request.xhr? }
313 format.html { render :layout => false if request.xhr? }
324 format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") }
314 format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") }
325 end
315 end
326 end
316 end
327
317
328 def calendar
318 def calendar
329 @trackers = @project.rolled_up_trackers
319 @trackers = @project.rolled_up_trackers
330 retrieve_selected_tracker_ids(@trackers)
320 retrieve_selected_tracker_ids(@trackers)
331
321
332 if params[:year] and params[:year].to_i > 1900
322 if params[:year] and params[:year].to_i > 1900
333 @year = params[:year].to_i
323 @year = params[:year].to_i
334 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
324 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
335 @month = params[:month].to_i
325 @month = params[:month].to_i
336 end
326 end
337 end
327 end
338 @year ||= Date.today.year
328 @year ||= Date.today.year
339 @month ||= Date.today.month
329 @month ||= Date.today.month
340 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
330 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
341 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
331 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
342 events = []
332 events = []
343 @project.issues_with_subprojects(@with_subprojects) do
333 @project.issues_with_subprojects(@with_subprojects) do
344 events += Issue.find(:all,
334 events += Issue.find(:all,
345 :include => [:tracker, :status, :assigned_to, :priority, :project],
335 :include => [:tracker, :status, :assigned_to, :priority, :project],
346 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
336 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
347 ) unless @selected_tracker_ids.empty?
337 ) unless @selected_tracker_ids.empty?
348 events += Version.find(:all, :include => :project,
338 events += Version.find(:all, :include => :project,
349 :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
339 :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
350 end
340 end
351 @calendar.events = events
341 @calendar.events = events
352
342
353 render :layout => false if request.xhr?
343 render :layout => false if request.xhr?
354 end
344 end
355
345
356 def gantt
346 def gantt
357 @trackers = @project.rolled_up_trackers
347 @trackers = @project.rolled_up_trackers
358 retrieve_selected_tracker_ids(@trackers)
348 retrieve_selected_tracker_ids(@trackers)
359
349
360 if params[:year] and params[:year].to_i >0
350 if params[:year] and params[:year].to_i >0
361 @year_from = params[:year].to_i
351 @year_from = params[:year].to_i
362 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
352 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
363 @month_from = params[:month].to_i
353 @month_from = params[:month].to_i
364 else
354 else
365 @month_from = 1
355 @month_from = 1
366 end
356 end
367 else
357 else
368 @month_from ||= Date.today.month
358 @month_from ||= Date.today.month
369 @year_from ||= Date.today.year
359 @year_from ||= Date.today.year
370 end
360 end
371
361
372 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
362 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
373 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
363 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
374 months = (params[:months] || User.current.pref[:gantt_months]).to_i
364 months = (params[:months] || User.current.pref[:gantt_months]).to_i
375 @months = (months > 0 && months < 25) ? months : 6
365 @months = (months > 0 && months < 25) ? months : 6
376
366
377 # Save gantt paramters as user preference (zoom and months count)
367 # Save gantt paramters as user preference (zoom and months count)
378 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
368 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
379 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
369 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
380 User.current.preference.save
370 User.current.preference.save
381 end
371 end
382
372
383 @date_from = Date.civil(@year_from, @month_from, 1)
373 @date_from = Date.civil(@year_from, @month_from, 1)
384 @date_to = (@date_from >> @months) - 1
374 @date_to = (@date_from >> @months) - 1
385 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
375 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
386
376
387 @events = []
377 @events = []
388 @project.issues_with_subprojects(@with_subprojects) do
378 @project.issues_with_subprojects(@with_subprojects) do
389 # Issues that have start and due dates
379 # Issues that have start and due dates
390 @events += Issue.find(:all,
380 @events += Issue.find(:all,
391 :order => "start_date, due_date",
381 :order => "start_date, due_date",
392 :include => [:tracker, :status, :assigned_to, :priority, :project],
382 :include => [:tracker, :status, :assigned_to, :priority, :project],
393 :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 and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
383 :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 and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
394 ) unless @selected_tracker_ids.empty?
384 ) unless @selected_tracker_ids.empty?
395 # Issues that don't have a due date but that are assigned to a version with a date
385 # Issues that don't have a due date but that are assigned to a version with a date
396 @events += Issue.find(:all,
386 @events += Issue.find(:all,
397 :order => "start_date, effective_date",
387 :order => "start_date, effective_date",
398 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
388 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
399 :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 and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
389 :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 and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
400 ) unless @selected_tracker_ids.empty?
390 ) unless @selected_tracker_ids.empty?
401 @events += Version.find(:all, :include => :project,
391 @events += Version.find(:all, :include => :project,
402 :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
392 :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
403 end
393 end
404 @events.sort! {|x,y| x.start_date <=> y.start_date }
394 @events.sort! {|x,y| x.start_date <=> y.start_date }
405
395
406 if params[:format]=='pdf'
396 if params[:format]=='pdf'
407 @options_for_rfpdf ||= {}
397 @options_for_rfpdf ||= {}
408 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
398 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
409 render :template => "projects/gantt.rfpdf", :layout => false
399 render :template => "projects/gantt.rfpdf", :layout => false
410 elsif params[:format]=='png' && respond_to?('gantt_image')
400 elsif params[:format]=='png' && respond_to?('gantt_image')
411 image = gantt_image(@events, @date_from, @months, @zoom)
401 image = gantt_image(@events, @date_from, @months, @zoom)
412 image.format = 'PNG'
402 image.format = 'PNG'
413 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
403 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
414 else
404 else
415 render :template => "projects/gantt.rhtml"
405 render :template => "projects/gantt.rhtml"
416 end
406 end
417 end
407 end
418
408
419 private
409 private
420 # Find project of id params[:id]
410 # Find project of id params[:id]
421 # if not found, redirect to project list
411 # if not found, redirect to project list
422 # Used as a before_filter
412 # Used as a before_filter
423 def find_project
413 def find_project
424 @project = Project.find(params[:id])
414 @project = Project.find(params[:id])
425 rescue ActiveRecord::RecordNotFound
415 rescue ActiveRecord::RecordNotFound
426 render_404
416 render_404
427 end
417 end
428
418
429 def find_optional_project
419 def find_optional_project
430 return true unless params[:id]
420 return true unless params[:id]
431 @project = Project.find(params[:id])
421 @project = Project.find(params[:id])
432 authorize
422 authorize
433 rescue ActiveRecord::RecordNotFound
423 rescue ActiveRecord::RecordNotFound
434 render_404
424 render_404
435 end
425 end
436
426
437 def retrieve_selected_tracker_ids(selectable_trackers)
427 def retrieve_selected_tracker_ids(selectable_trackers)
438 if ids = params[:tracker_ids]
428 if ids = params[:tracker_ids]
439 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
429 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
440 else
430 else
441 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
431 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
442 end
432 end
443 end
433 end
444 end
434 end
@@ -1,262 +1,262
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 menu_item :issues
21 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
22
22
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :issues
27 helper :issues
28 include TimelogHelper
28 include TimelogHelper
29 helper :custom_fields
29 helper :custom_fields
30 include CustomFieldsHelper
30 include CustomFieldsHelper
31
31
32 def report
32 def report
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
34 :klass => Project,
34 :klass => Project,
35 :label => :label_project},
35 :label => :label_project},
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
37 :klass => Version,
37 :klass => Version,
38 :label => :label_version},
38 :label => :label_version},
39 'category' => {:sql => "#{Issue.table_name}.category_id",
39 'category' => {:sql => "#{Issue.table_name}.category_id",
40 :klass => IssueCategory,
40 :klass => IssueCategory,
41 :label => :field_category},
41 :label => :field_category},
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
43 :klass => User,
43 :klass => User,
44 :label => :label_member},
44 :label => :label_member},
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
46 :klass => Tracker,
46 :klass => Tracker,
47 :label => :label_tracker},
47 :label => :label_tracker},
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
49 :klass => Enumeration,
49 :klass => Enumeration,
50 :label => :label_activity},
50 :label => :label_activity},
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
52 :klass => Issue,
52 :klass => Issue,
53 :label => :label_issue}
53 :label => :label_issue}
54 }
54 }
55
55
56 # Add list and boolean custom fields as available criterias
56 # Add list and boolean custom fields as available criterias
57 @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
57 @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
58 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
58 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
59 :format => cf.field_format,
59 :format => cf.field_format,
60 :label => cf.name}
60 :label => cf.name}
61 end
61 end
62
62
63 @criterias = params[:criterias] || []
63 @criterias = params[:criterias] || []
64 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
64 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
65 @criterias.uniq!
65 @criterias.uniq!
66 @criterias = @criterias[0,3]
66 @criterias = @criterias[0,3]
67
67
68 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
68 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
69
69
70 retrieve_date_range
70 retrieve_date_range
71
71
72 unless @criterias.empty?
72 unless @criterias.empty?
73 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
73 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
74 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
74 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
75
75
76 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
76 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
77 sql << " FROM #{TimeEntry.table_name}"
77 sql << " FROM #{TimeEntry.table_name}"
78 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
78 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
79 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
79 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
80 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
80 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
81 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
81 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
82 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
82 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
83 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
83 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
84
84
85 @hours = ActiveRecord::Base.connection.select_all(sql)
85 @hours = ActiveRecord::Base.connection.select_all(sql)
86
86
87 @hours.each do |row|
87 @hours.each do |row|
88 case @columns
88 case @columns
89 when 'year'
89 when 'year'
90 row['year'] = row['tyear']
90 row['year'] = row['tyear']
91 when 'month'
91 when 'month'
92 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
92 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
93 when 'week'
93 when 'week'
94 row['week'] = "#{row['tyear']}-#{row['tweek']}"
94 row['week'] = "#{row['tyear']}-#{row['tweek']}"
95 when 'day'
95 when 'day'
96 row['day'] = "#{row['spent_on']}"
96 row['day'] = "#{row['spent_on']}"
97 end
97 end
98 end
98 end
99
99
100 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
100 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
101
101
102 @periods = []
102 @periods = []
103 # Date#at_beginning_of_ not supported in Rails 1.2.x
103 # Date#at_beginning_of_ not supported in Rails 1.2.x
104 date_from = @from.to_time
104 date_from = @from.to_time
105 # 100 columns max
105 # 100 columns max
106 while date_from <= @to.to_time && @periods.length < 100
106 while date_from <= @to.to_time && @periods.length < 100
107 case @columns
107 case @columns
108 when 'year'
108 when 'year'
109 @periods << "#{date_from.year}"
109 @periods << "#{date_from.year}"
110 date_from = (date_from + 1.year).at_beginning_of_year
110 date_from = (date_from + 1.year).at_beginning_of_year
111 when 'month'
111 when 'month'
112 @periods << "#{date_from.year}-#{date_from.month}"
112 @periods << "#{date_from.year}-#{date_from.month}"
113 date_from = (date_from + 1.month).at_beginning_of_month
113 date_from = (date_from + 1.month).at_beginning_of_month
114 when 'week'
114 when 'week'
115 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
115 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
116 date_from = (date_from + 7.day).at_beginning_of_week
116 date_from = (date_from + 7.day).at_beginning_of_week
117 when 'day'
117 when 'day'
118 @periods << "#{date_from.to_date}"
118 @periods << "#{date_from.to_date}"
119 date_from = date_from + 1.day
119 date_from = date_from + 1.day
120 end
120 end
121 end
121 end
122 end
122 end
123
123
124 respond_to do |format|
124 respond_to do |format|
125 format.html { render :layout => !request.xhr? }
125 format.html { render :layout => !request.xhr? }
126 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
126 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
127 end
127 end
128 end
128 end
129
129
130 def details
130 def details
131 sort_init 'spent_on', 'desc'
131 sort_init 'spent_on', 'desc'
132 sort_update
132 sort_update
133
133
134 cond = ARCondition.new
134 cond = ARCondition.new
135 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
135 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
136 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
136 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
137
137
138 retrieve_date_range
138 retrieve_date_range
139 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
139 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
140
140
141 TimeEntry.visible_by(User.current) do
141 TimeEntry.visible_by(User.current) do
142 respond_to do |format|
142 respond_to do |format|
143 format.html {
143 format.html {
144 # Paginate results
144 # Paginate results
145 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
145 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
146 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
146 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
147 @entries = TimeEntry.find(:all,
147 @entries = TimeEntry.find(:all,
148 :include => [:project, :activity, :user, {:issue => :tracker}],
148 :include => [:project, :activity, :user, {:issue => :tracker}],
149 :conditions => cond.conditions,
149 :conditions => cond.conditions,
150 :order => sort_clause,
150 :order => sort_clause,
151 :limit => @entry_pages.items_per_page,
151 :limit => @entry_pages.items_per_page,
152 :offset => @entry_pages.current.offset)
152 :offset => @entry_pages.current.offset)
153 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
153 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
154
154
155 render :layout => !request.xhr?
155 render :layout => !request.xhr?
156 }
156 }
157 format.atom {
157 format.atom {
158 entries = TimeEntry.find(:all,
158 entries = TimeEntry.find(:all,
159 :include => [:project, :activity, :user, {:issue => :tracker}],
159 :include => [:project, :activity, :user, {:issue => :tracker}],
160 :conditions => cond.conditions,
160 :conditions => cond.conditions,
161 :order => "#{TimeEntry.table_name}.created_on DESC",
161 :order => "#{TimeEntry.table_name}.created_on DESC",
162 :limit => Setting.feeds_limit.to_i)
162 :limit => Setting.feeds_limit.to_i)
163 render_feed(entries, :title => l(:label_spent_time))
163 render_feed(entries, :title => l(:label_spent_time))
164 }
164 }
165 format.csv {
165 format.csv {
166 # Export all entries
166 # Export all entries
167 @entries = TimeEntry.find(:all,
167 @entries = TimeEntry.find(:all,
168 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
168 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
169 :conditions => cond.conditions,
169 :conditions => cond.conditions,
170 :order => sort_clause)
170 :order => sort_clause)
171 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
171 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
172 }
172 }
173 end
173 end
174 end
174 end
175 end
175 end
176
176
177 def edit
177 def edit
178 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
178 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
179 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
179 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
180 @time_entry.attributes = params[:time_entry]
180 @time_entry.attributes = params[:time_entry]
181 if request.post? and @time_entry.save
181 if request.post? and @time_entry.save
182 flash[:notice] = l(:notice_successful_update)
182 flash[:notice] = l(:notice_successful_update)
183 redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
183 redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
184 return
184 return
185 end
185 end
186 @activities = Enumeration::get_values('ACTI')
186 @activities = Enumeration::get_values('ACTI')
187 end
187 end
188
188
189 def destroy
189 def destroy
190 render_404 and return unless @time_entry
190 render_404 and return unless @time_entry
191 render_403 and return unless @time_entry.editable_by?(User.current)
191 render_403 and return unless @time_entry.editable_by?(User.current)
192 @time_entry.destroy
192 @time_entry.destroy
193 flash[:notice] = l(:notice_successful_delete)
193 flash[:notice] = l(:notice_successful_delete)
194 redirect_to :back
194 redirect_to :back
195 rescue RedirectBackError
195 rescue RedirectBackError
196 redirect_to :action => 'details', :project_id => @time_entry.project
196 redirect_to :action => 'details', :project_id => @time_entry.project
197 end
197 end
198
198
199 private
199 private
200 def find_project
200 def find_project
201 if params[:id]
201 if params[:id]
202 @time_entry = TimeEntry.find(params[:id])
202 @time_entry = TimeEntry.find(params[:id])
203 @project = @time_entry.project
203 @project = @time_entry.project
204 elsif params[:issue_id]
204 elsif params[:issue_id]
205 @issue = Issue.find(params[:issue_id])
205 @issue = Issue.find(params[:issue_id])
206 @project = @issue.project
206 @project = @issue.project
207 elsif params[:project_id]
207 elsif params[:project_id]
208 @project = Project.find(params[:project_id])
208 @project = Project.find(params[:project_id])
209 else
209 else
210 render_404
210 render_404
211 return false
211 return false
212 end
212 end
213 rescue ActiveRecord::RecordNotFound
213 rescue ActiveRecord::RecordNotFound
214 render_404
214 render_404
215 end
215 end
216
216
217 # Retrieves the date range based on predefined ranges or specific from/to param dates
217 # Retrieves the date range based on predefined ranges or specific from/to param dates
218 def retrieve_date_range
218 def retrieve_date_range
219 @free_period = false
219 @free_period = false
220 @from, @to = nil, nil
220 @from, @to = nil, nil
221
221
222 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
222 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
223 case params[:period].to_s
223 case params[:period].to_s
224 when 'today'
224 when 'today'
225 @from = @to = Date.today
225 @from = @to = Date.today
226 when 'yesterday'
226 when 'yesterday'
227 @from = @to = Date.today - 1
227 @from = @to = Date.today - 1
228 when 'current_week'
228 when 'current_week'
229 @from = Date.today - (Date.today.cwday - 1)%7
229 @from = Date.today - (Date.today.cwday - 1)%7
230 @to = @from + 6
230 @to = @from + 6
231 when 'last_week'
231 when 'last_week'
232 @from = Date.today - 7 - (Date.today.cwday - 1)%7
232 @from = Date.today - 7 - (Date.today.cwday - 1)%7
233 @to = @from + 6
233 @to = @from + 6
234 when '7_days'
234 when '7_days'
235 @from = Date.today - 7
235 @from = Date.today - 7
236 @to = Date.today
236 @to = Date.today
237 when 'current_month'
237 when 'current_month'
238 @from = Date.civil(Date.today.year, Date.today.month, 1)
238 @from = Date.civil(Date.today.year, Date.today.month, 1)
239 @to = (@from >> 1) - 1
239 @to = (@from >> 1) - 1
240 when 'last_month'
240 when 'last_month'
241 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
241 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
242 @to = (@from >> 1) - 1
242 @to = (@from >> 1) - 1
243 when '30_days'
243 when '30_days'
244 @from = Date.today - 30
244 @from = Date.today - 30
245 @to = Date.today
245 @to = Date.today
246 when 'current_year'
246 when 'current_year'
247 @from = Date.civil(Date.today.year, 1, 1)
247 @from = Date.civil(Date.today.year, 1, 1)
248 @to = Date.civil(Date.today.year, 12, 31)
248 @to = Date.civil(Date.today.year, 12, 31)
249 end
249 end
250 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
250 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
251 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
251 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
252 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
252 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
253 @free_period = true
253 @free_period = true
254 else
254 else
255 # default
255 # default
256 end
256 end
257
257
258 @from, @to = @to, @from if @from && @to && @from > @to
258 @from, @to = @to, @from if @from && @to && @from > @to
259 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
259 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
260 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
260 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
261 end
261 end
262 end
262 end
@@ -1,110 +1,101
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :require_admin
20 before_filter :require_admin
21
21
22 helper :sort
22 helper :sort
23 include SortHelper
23 include SortHelper
24 helper :custom_fields
24 helper :custom_fields
25 include CustomFieldsHelper
25 include CustomFieldsHelper
26
26
27 def index
27 def index
28 list
28 list
29 render :action => 'list' unless request.xhr?
29 render :action => 'list' unless request.xhr?
30 end
30 end
31
31
32 def list
32 def list
33 sort_init 'login', 'asc'
33 sort_init 'login', 'asc'
34 sort_update
34 sort_update
35
35
36 @status = params[:status] ? params[:status].to_i : 1
36 @status = params[:status] ? params[:status].to_i : 1
37 conditions = "status <> 0"
37 conditions = "status <> 0"
38 conditions = ["status=?", @status] unless @status == 0
38 conditions = ["status=?", @status] unless @status == 0
39
39
40 @user_count = User.count(:conditions => conditions)
40 @user_count = User.count(:conditions => conditions)
41 @user_pages = Paginator.new self, @user_count,
41 @user_pages = Paginator.new self, @user_count,
42 per_page_option,
42 per_page_option,
43 params['page']
43 params['page']
44 @users = User.find :all,:order => sort_clause,
44 @users = User.find :all,:order => sort_clause,
45 :conditions => conditions,
45 :conditions => conditions,
46 :limit => @user_pages.items_per_page,
46 :limit => @user_pages.items_per_page,
47 :offset => @user_pages.current.offset
47 :offset => @user_pages.current.offset
48
48
49 render :action => "list", :layout => false if request.xhr?
49 render :action => "list", :layout => false if request.xhr?
50 end
50 end
51
51
52 def add
52 def add
53 if request.get?
53 if request.get?
54 @user = User.new(:language => Setting.default_language)
54 @user = User.new(:language => Setting.default_language)
55 @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
56 else
55 else
57 @user = User.new(params[:user])
56 @user = User.new(params[:user])
58 @user.admin = params[:user][:admin] || false
57 @user.admin = params[:user][:admin] || false
59 @user.login = params[:user][:login]
58 @user.login = params[:user][:login]
60 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
59 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
61 @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
62 @user.custom_values = @custom_values
63 if @user.save
60 if @user.save
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
61 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 flash[:notice] = l(:notice_successful_create)
62 flash[:notice] = l(:notice_successful_create)
66 redirect_to :action => 'list'
63 redirect_to :action => 'list'
67 end
64 end
68 end
65 end
69 @auth_sources = AuthSource.find(:all)
66 @auth_sources = AuthSource.find(:all)
70 end
67 end
71
68
72 def edit
69 def edit
73 @user = User.find(params[:id])
70 @user = User.find(params[:id])
74 if request.get?
71 if request.post?
75 @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
76 else
77 @user.admin = params[:user][:admin] if params[:user][:admin]
72 @user.admin = params[:user][:admin] if params[:user][:admin]
78 @user.login = params[:user][:login] if params[:user][:login]
73 @user.login = params[:user][:login] if params[:user][:login]
79 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
74 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
80 if params[:custom_fields]
81 @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
82 @user.custom_values = @custom_values
83 end
84 if @user.update_attributes(params[:user])
75 if @user.update_attributes(params[:user])
85 flash[:notice] = l(:notice_successful_update)
76 flash[:notice] = l(:notice_successful_update)
86 # Give a string to redirect_to otherwise it would use status param as the response code
77 # Give a string to redirect_to otherwise it would use status param as the response code
87 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
78 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
88 end
79 end
89 end
80 end
90 @auth_sources = AuthSource.find(:all)
81 @auth_sources = AuthSource.find(:all)
91 @roles = Role.find_all_givable
82 @roles = Role.find_all_givable
92 @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
83 @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
93 @membership ||= Member.new
84 @membership ||= Member.new
94 @memberships = @user.memberships
85 @memberships = @user.memberships
95 end
86 end
96
87
97 def edit_membership
88 def edit_membership
98 @user = User.find(params[:id])
89 @user = User.find(params[:id])
99 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
90 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
100 @membership.attributes = params[:membership]
91 @membership.attributes = params[:membership]
101 @membership.save if request.post?
92 @membership.save if request.post?
102 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
93 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
103 end
94 end
104
95
105 def destroy_membership
96 def destroy_membership
106 @user = User.find(params[:id])
97 @user = User.find(params[:id])
107 Member.find(params[:membership_id]).destroy if request.post?
98 Member.find(params[:membership_id]).destroy if request.post?
108 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
99 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
109 end
100 end
110 end
101 end
@@ -1,84 +1,87
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module CustomFieldsHelper
18 module CustomFieldsHelper
19
19
20 def custom_fields_tabs
20 def custom_fields_tabs
21 tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
21 tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
22 {:name => 'ProjectCustomField', :label => :label_project_plural},
22 {:name => 'ProjectCustomField', :label => :label_project_plural},
23 {:name => 'UserCustomField', :label => :label_user_plural}
23 {:name => 'UserCustomField', :label => :label_user_plural}
24 ]
24 ]
25 end
25 end
26
26
27 # Return custom field html tag corresponding to its format
27 # Return custom field html tag corresponding to its format
28 def custom_field_tag(custom_value)
28 def custom_field_tag(name, custom_value)
29 custom_field = custom_value.custom_field
29 custom_field = custom_value.custom_field
30 field_name = "custom_fields[#{custom_field.id}]"
30 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
31 field_id = "custom_fields_#{custom_field.id}"
31 field_id = "#{name}_custom_field_values_#{custom_field.id}"
32
32
33 case custom_field.field_format
33 case custom_field.field_format
34 when "date"
34 when "date"
35 text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) +
35 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
36 calendar_for(field_id)
36 calendar_for(field_id)
37 when "text"
37 when "text"
38 text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%'
38 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
39 when "bool"
39 when "bool"
40 check_box 'custom_value', 'value', :name => field_name, :id => field_id
40 check_box_tag(field_name, custom_value.value, :id => field_id)
41 when "list"
41 when "list"
42 select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id
42 blank_option = custom_field.is_required? ?
43 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
44 '<option></option>'
45 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
43 else
46 else
44 text_field 'custom_value', 'value', :name => field_name, :id => field_id
47 text_field_tag(field_name, custom_value.value, :id => field_id)
45 end
48 end
46 end
49 end
47
50
48 # Return custom field label tag
51 # Return custom field label tag
49 def custom_field_label_tag(custom_value)
52 def custom_field_label_tag(name, custom_value)
50 content_tag "label", custom_value.custom_field.name +
53 content_tag "label", custom_value.custom_field.name +
51 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
54 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
52 :for => "custom_fields_#{custom_value.custom_field.id}",
55 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
53 :class => (custom_value.errors.empty? ? nil : "error" )
56 :class => (custom_value.errors.empty? ? nil : "error" )
54 end
57 end
55
58
56 # Return custom field tag with its label tag
59 # Return custom field tag with its label tag
57 def custom_field_tag_with_label(custom_value)
60 def custom_field_tag_with_label(name, custom_value)
58 custom_field_label_tag(custom_value) + custom_field_tag(custom_value)
61 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
59 end
62 end
60
63
61 # Return a string used to display a custom value
64 # Return a string used to display a custom value
62 def show_value(custom_value)
65 def show_value(custom_value)
63 return "" unless custom_value
66 return "" unless custom_value
64 format_value(custom_value.value, custom_value.custom_field.field_format)
67 format_value(custom_value.value, custom_value.custom_field.field_format)
65 end
68 end
66
69
67 # Return a string used to display a custom value
70 # Return a string used to display a custom value
68 def format_value(value, field_format)
71 def format_value(value, field_format)
69 return "" unless value && !value.empty?
72 return "" unless value && !value.empty?
70 case field_format
73 case field_format
71 when "date"
74 when "date"
72 begin; format_date(value.to_date); rescue; value end
75 begin; format_date(value.to_date); rescue; value end
73 when "bool"
76 when "bool"
74 l_YesNo(value == "1")
77 l_YesNo(value == "1")
75 else
78 else
76 value
79 value
77 end
80 end
78 end
81 end
79
82
80 # Return an array of custom field formats which can be used in select_tag
83 # Return an array of custom field formats which can be used in select_tag
81 def custom_field_formats_for_select
84 def custom_field_formats_for_select
82 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
85 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
83 end
86 end
84 end
87 end
@@ -1,184 +1,184
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
35
36 def sidebar_queries
36 def sidebar_queries
37 unless @sidebar_queries
37 unless @sidebar_queries
38 # User can see public queries and his own queries
38 # User can see public queries and his own queries
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
40 # Project specific queries and global queries
40 # Project specific queries and global queries
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
42 @sidebar_queries = Query.find(:all,
42 @sidebar_queries = Query.find(:all,
43 :order => "name ASC",
43 :order => "name ASC",
44 :conditions => visible.conditions)
44 :conditions => visible.conditions)
45 end
45 end
46 @sidebar_queries
46 @sidebar_queries
47 end
47 end
48
48
49 def show_detail(detail, no_html=false)
49 def show_detail(detail, no_html=false)
50 case detail.property
50 case detail.property
51 when 'attr'
51 when 'attr'
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
53 case detail.prop_key
53 case detail.prop_key
54 when 'due_date', 'start_date'
54 when 'due_date', 'start_date'
55 value = format_date(detail.value.to_date) if detail.value
55 value = format_date(detail.value.to_date) if detail.value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
57 when 'project_id'
57 when 'project_id'
58 p = Project.find_by_id(detail.value) and value = p.name if detail.value
58 p = Project.find_by_id(detail.value) and value = p.name if detail.value
59 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
59 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
60 when 'status_id'
60 when 'status_id'
61 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
61 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
62 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
62 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
63 when 'tracker_id'
63 when 'tracker_id'
64 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
64 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
65 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
65 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
66 when 'assigned_to_id'
66 when 'assigned_to_id'
67 u = User.find_by_id(detail.value) and value = u.name if detail.value
67 u = User.find_by_id(detail.value) and value = u.name if detail.value
68 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
68 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
69 when 'priority_id'
69 when 'priority_id'
70 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
70 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
71 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
71 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
72 when 'category_id'
72 when 'category_id'
73 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
73 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
74 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
74 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
75 when 'fixed_version_id'
75 when 'fixed_version_id'
76 v = Version.find_by_id(detail.value) and value = v.name if detail.value
76 v = Version.find_by_id(detail.value) and value = v.name if detail.value
77 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
77 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
78 end
78 end
79 when 'cf'
79 when 'cf'
80 custom_field = CustomField.find_by_id(detail.prop_key)
80 custom_field = CustomField.find_by_id(detail.prop_key)
81 if custom_field
81 if custom_field
82 label = custom_field.name
82 label = custom_field.name
83 value = format_value(detail.value, custom_field.field_format) if detail.value
83 value = format_value(detail.value, custom_field.field_format) if detail.value
84 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
84 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
85 end
85 end
86 when 'attachment'
86 when 'attachment'
87 label = l(:label_attachment)
87 label = l(:label_attachment)
88 end
88 end
89
89
90 label ||= detail.prop_key
90 label ||= detail.prop_key
91 value ||= detail.value
91 value ||= detail.value
92 old_value ||= detail.old_value
92 old_value ||= detail.old_value
93
93
94 unless no_html
94 unless no_html
95 label = content_tag('strong', label)
95 label = content_tag('strong', label)
96 old_value = content_tag("i", h(old_value)) if detail.old_value
96 old_value = content_tag("i", h(old_value)) if detail.old_value
97 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
97 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
98 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
98 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
99 # Link to the attachment if it has not been removed
99 # Link to the attachment if it has not been removed
100 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
100 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
101 else
101 else
102 value = content_tag("i", h(value)) if value
102 value = content_tag("i", h(value)) if value
103 end
103 end
104 end
104 end
105
105
106 if !detail.value.blank?
106 if !detail.value.blank?
107 case detail.property
107 case detail.property
108 when 'attr', 'cf'
108 when 'attr', 'cf'
109 if !detail.old_value.blank?
109 if !detail.old_value.blank?
110 label + " " + l(:text_journal_changed, old_value, value)
110 label + " " + l(:text_journal_changed, old_value, value)
111 else
111 else
112 label + " " + l(:text_journal_set_to, value)
112 label + " " + l(:text_journal_set_to, value)
113 end
113 end
114 when 'attachment'
114 when 'attachment'
115 "#{label} #{value} #{l(:label_added)}"
115 "#{label} #{value} #{l(:label_added)}"
116 end
116 end
117 else
117 else
118 case detail.property
118 case detail.property
119 when 'attr', 'cf'
119 when 'attr', 'cf'
120 label + " " + l(:text_journal_deleted) + " (#{old_value})"
120 label + " " + l(:text_journal_deleted) + " (#{old_value})"
121 when 'attachment'
121 when 'attachment'
122 "#{label} #{old_value} #{l(:label_deleted)}"
122 "#{label} #{old_value} #{l(:label_deleted)}"
123 end
123 end
124 end
124 end
125 end
125 end
126
126
127 def issues_to_csv(issues, project = nil)
127 def issues_to_csv(issues, project = nil)
128 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
128 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
129 decimal_separator = l(:general_csv_decimal_separator)
129 decimal_separator = l(:general_csv_decimal_separator)
130 export = StringIO.new
130 export = StringIO.new
131 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
131 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
132 # csv header fields
132 # csv header fields
133 headers = [ "#",
133 headers = [ "#",
134 l(:field_status),
134 l(:field_status),
135 l(:field_project),
135 l(:field_project),
136 l(:field_tracker),
136 l(:field_tracker),
137 l(:field_priority),
137 l(:field_priority),
138 l(:field_subject),
138 l(:field_subject),
139 l(:field_assigned_to),
139 l(:field_assigned_to),
140 l(:field_category),
140 l(:field_category),
141 l(:field_fixed_version),
141 l(:field_fixed_version),
142 l(:field_author),
142 l(:field_author),
143 l(:field_start_date),
143 l(:field_start_date),
144 l(:field_due_date),
144 l(:field_due_date),
145 l(:field_done_ratio),
145 l(:field_done_ratio),
146 l(:field_estimated_hours),
146 l(:field_estimated_hours),
147 l(:field_created_on),
147 l(:field_created_on),
148 l(:field_updated_on)
148 l(:field_updated_on)
149 ]
149 ]
150 # Export project custom fields if project is given
150 # Export project custom fields if project is given
151 # otherwise export custom fields marked as "For all projects"
151 # otherwise export custom fields marked as "For all projects"
152 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
152 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
153 custom_fields.each {|f| headers << f.name}
153 custom_fields.each {|f| headers << f.name}
154 # Description in the last column
154 # Description in the last column
155 headers << l(:field_description)
155 headers << l(:field_description)
156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
157 # csv lines
157 # csv lines
158 issues.each do |issue|
158 issues.each do |issue|
159 fields = [issue.id,
159 fields = [issue.id,
160 issue.status.name,
160 issue.status.name,
161 issue.project.name,
161 issue.project.name,
162 issue.tracker.name,
162 issue.tracker.name,
163 issue.priority.name,
163 issue.priority.name,
164 issue.subject,
164 issue.subject,
165 issue.assigned_to,
165 issue.assigned_to,
166 issue.category,
166 issue.category,
167 issue.fixed_version,
167 issue.fixed_version,
168 issue.author.name,
168 issue.author.name,
169 format_date(issue.start_date),
169 format_date(issue.start_date),
170 format_date(issue.due_date),
170 format_date(issue.due_date),
171 issue.done_ratio,
171 issue.done_ratio,
172 issue.estimated_hours.to_s.gsub('.', decimal_separator),
172 issue.estimated_hours.to_s.gsub('.', decimal_separator),
173 format_time(issue.created_on),
173 format_time(issue.created_on),
174 format_time(issue.updated_on)
174 format_time(issue.updated_on)
175 ]
175 ]
176 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
176 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
177 fields << issue.description
177 fields << issue.description
178 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
178 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
179 end
179 end
180 end
180 end
181 export.rewind
181 export.rewind
182 export
182 export
183 end
183 end
184 end
184 end
@@ -1,256 +1,254
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 has_many :custom_fields, :through => :custom_values
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
31 has_and_belongs_to_many :changesets, :order => "revision ASC"
34
32
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37
35
36 acts_as_customizable
38 acts_as_watchable
37 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
38 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
39 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
40 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42
41
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
42 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 validates_length_of :subject, :maximum => 255
43 validates_length_of :subject, :maximum => 255
45 validates_inclusion_of :done_ratio, :in => 0..100
44 validates_inclusion_of :done_ratio, :in => 0..100
46 validates_numericality_of :estimated_hours, :allow_nil => true
45 validates_numericality_of :estimated_hours, :allow_nil => true
47 validates_associated :custom_values, :on => :update
48
46
49 def after_initialize
47 def after_initialize
50 if new_record?
48 if new_record?
51 # set default values for new records only
49 # set default values for new records only
52 self.status ||= IssueStatus.default
50 self.status ||= IssueStatus.default
53 self.priority ||= Enumeration.default('IPRI')
51 self.priority ||= Enumeration.default('IPRI')
54 end
52 end
55 end
53 end
56
54
55 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
56 def available_custom_fields
57 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
58 end
59
57 def copy_from(arg)
60 def copy_from(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
61 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 self.attributes = issue.attributes.dup
62 self.attributes = issue.attributes.dup
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
63 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 self
64 self
62 end
65 end
63
66
64 # Move an issue to a new project and tracker
67 # Move an issue to a new project and tracker
65 def move_to(new_project, new_tracker = nil)
68 def move_to(new_project, new_tracker = nil)
66 transaction do
69 transaction do
67 if new_project && project_id != new_project.id
70 if new_project && project_id != new_project.id
68 # delete issue relations
71 # delete issue relations
69 unless Setting.cross_project_issue_relations?
72 unless Setting.cross_project_issue_relations?
70 self.relations_from.clear
73 self.relations_from.clear
71 self.relations_to.clear
74 self.relations_to.clear
72 end
75 end
73 # issue is moved to another project
76 # issue is moved to another project
74 self.category = nil
77 self.category = nil
75 self.fixed_version = nil
78 self.fixed_version = nil
76 self.project = new_project
79 self.project = new_project
77 end
80 end
78 if new_tracker
81 if new_tracker
79 self.tracker = new_tracker
82 self.tracker = new_tracker
80 end
83 end
81 if save
84 if save
82 # Manually update project_id on related time entries
85 # Manually update project_id on related time entries
83 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
86 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
84 else
87 else
85 rollback_db_transaction
88 rollback_db_transaction
86 return false
89 return false
87 end
90 end
88 end
91 end
89 return true
92 return true
90 end
93 end
91
94
92 def priority_id=(pid)
95 def priority_id=(pid)
93 self.priority = nil
96 self.priority = nil
94 write_attribute(:priority_id, pid)
97 write_attribute(:priority_id, pid)
95 end
98 end
96
99
97 def estimated_hours=(h)
100 def estimated_hours=(h)
98 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
101 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
99 end
102 end
100
103
101 def validate
104 def validate
102 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
105 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
103 errors.add :due_date, :activerecord_error_not_a_date
106 errors.add :due_date, :activerecord_error_not_a_date
104 end
107 end
105
108
106 if self.due_date and self.start_date and self.due_date < self.start_date
109 if self.due_date and self.start_date and self.due_date < self.start_date
107 errors.add :due_date, :activerecord_error_greater_than_start_date
110 errors.add :due_date, :activerecord_error_greater_than_start_date
108 end
111 end
109
112
110 if start_date && soonest_start && start_date < soonest_start
113 if start_date && soonest_start && start_date < soonest_start
111 errors.add :start_date, :activerecord_error_invalid
114 errors.add :start_date, :activerecord_error_invalid
112 end
115 end
113 end
116 end
114
117
115 def validate_on_create
118 def validate_on_create
116 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
119 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
117 end
120 end
118
121
119 def before_create
122 def before_create
120 # default assignment based on category
123 # default assignment based on category
121 if assigned_to.nil? && category && category.assigned_to
124 if assigned_to.nil? && category && category.assigned_to
122 self.assigned_to = category.assigned_to
125 self.assigned_to = category.assigned_to
123 end
126 end
124 end
127 end
125
128
126 def before_save
129 def before_save
127 if @current_journal
130 if @current_journal
128 # attributes changes
131 # attributes changes
129 (Issue.column_names - %w(id description)).each {|c|
132 (Issue.column_names - %w(id description)).each {|c|
130 @current_journal.details << JournalDetail.new(:property => 'attr',
133 @current_journal.details << JournalDetail.new(:property => 'attr',
131 :prop_key => c,
134 :prop_key => c,
132 :old_value => @issue_before_change.send(c),
135 :old_value => @issue_before_change.send(c),
133 :value => send(c)) unless send(c)==@issue_before_change.send(c)
136 :value => send(c)) unless send(c)==@issue_before_change.send(c)
134 }
137 }
135 # custom fields changes
138 # custom fields changes
136 custom_values.each {|c|
139 custom_values.each {|c|
137 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
140 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
138 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
141 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
139 @current_journal.details << JournalDetail.new(:property => 'cf',
142 @current_journal.details << JournalDetail.new(:property => 'cf',
140 :prop_key => c.custom_field_id,
143 :prop_key => c.custom_field_id,
141 :old_value => @custom_values_before_change[c.custom_field_id],
144 :old_value => @custom_values_before_change[c.custom_field_id],
142 :value => c.value)
145 :value => c.value)
143 }
146 }
144 @current_journal.save
147 @current_journal.save
145 end
148 end
146 # Save the issue even if the journal is not saved (because empty)
149 # Save the issue even if the journal is not saved (because empty)
147 true
150 true
148 end
151 end
149
152
150 def after_save
153 def after_save
151 # Reload is needed in order to get the right status
154 # Reload is needed in order to get the right status
152 reload
155 reload
153
156
154 # Update start/due dates of following issues
157 # Update start/due dates of following issues
155 relations_from.each(&:set_issue_to_dates)
158 relations_from.each(&:set_issue_to_dates)
156
159
157 # Close duplicates if the issue was closed
160 # Close duplicates if the issue was closed
158 if @issue_before_change && !@issue_before_change.closed? && self.closed?
161 if @issue_before_change && !@issue_before_change.closed? && self.closed?
159 duplicates.each do |duplicate|
162 duplicates.each do |duplicate|
160 # Reload is need in case the duplicate was updated by a previous duplicate
163 # Reload is need in case the duplicate was updated by a previous duplicate
161 duplicate.reload
164 duplicate.reload
162 # Don't re-close it if it's already closed
165 # Don't re-close it if it's already closed
163 next if duplicate.closed?
166 next if duplicate.closed?
164 # Same user and notes
167 # Same user and notes
165 duplicate.init_journal(@current_journal.user, @current_journal.notes)
168 duplicate.init_journal(@current_journal.user, @current_journal.notes)
166 duplicate.update_attribute :status, self.status
169 duplicate.update_attribute :status, self.status
167 end
170 end
168 end
171 end
169 end
172 end
170
173
171 def custom_value_for(custom_field)
172 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
173 return nil
174 end
175
176 def init_journal(user, notes = "")
174 def init_journal(user, notes = "")
177 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
175 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
178 @issue_before_change = self.clone
176 @issue_before_change = self.clone
179 @issue_before_change.status = self.status
177 @issue_before_change.status = self.status
180 @custom_values_before_change = {}
178 @custom_values_before_change = {}
181 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
179 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
182 @current_journal
180 @current_journal
183 end
181 end
184
182
185 # Return true if the issue is closed, otherwise false
183 # Return true if the issue is closed, otherwise false
186 def closed?
184 def closed?
187 self.status.is_closed?
185 self.status.is_closed?
188 end
186 end
189
187
190 # Users the issue can be assigned to
188 # Users the issue can be assigned to
191 def assignable_users
189 def assignable_users
192 project.assignable_users
190 project.assignable_users
193 end
191 end
194
192
195 # Returns an array of status that user is able to apply
193 # Returns an array of status that user is able to apply
196 def new_statuses_allowed_to(user)
194 def new_statuses_allowed_to(user)
197 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
195 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
198 statuses << status unless statuses.empty?
196 statuses << status unless statuses.empty?
199 statuses.uniq.sort
197 statuses.uniq.sort
200 end
198 end
201
199
202 # Returns the mail adresses of users that should be notified for the issue
200 # Returns the mail adresses of users that should be notified for the issue
203 def recipients
201 def recipients
204 recipients = project.recipients
202 recipients = project.recipients
205 # Author and assignee are always notified unless they have been locked
203 # Author and assignee are always notified unless they have been locked
206 recipients << author.mail if author && author.active?
204 recipients << author.mail if author && author.active?
207 recipients << assigned_to.mail if assigned_to && assigned_to.active?
205 recipients << assigned_to.mail if assigned_to && assigned_to.active?
208 recipients.compact.uniq
206 recipients.compact.uniq
209 end
207 end
210
208
211 def spent_hours
209 def spent_hours
212 @spent_hours ||= time_entries.sum(:hours) || 0
210 @spent_hours ||= time_entries.sum(:hours) || 0
213 end
211 end
214
212
215 def relations
213 def relations
216 (relations_from + relations_to).sort
214 (relations_from + relations_to).sort
217 end
215 end
218
216
219 def all_dependent_issues
217 def all_dependent_issues
220 dependencies = []
218 dependencies = []
221 relations_from.each do |relation|
219 relations_from.each do |relation|
222 dependencies << relation.issue_to
220 dependencies << relation.issue_to
223 dependencies += relation.issue_to.all_dependent_issues
221 dependencies += relation.issue_to.all_dependent_issues
224 end
222 end
225 dependencies
223 dependencies
226 end
224 end
227
225
228 # Returns an array of issues that duplicate this one
226 # Returns an array of issues that duplicate this one
229 def duplicates
227 def duplicates
230 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
228 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
231 end
229 end
232
230
233 # Returns the due date or the target due date if any
231 # Returns the due date or the target due date if any
234 # Used on gantt chart
232 # Used on gantt chart
235 def due_before
233 def due_before
236 due_date || (fixed_version ? fixed_version.effective_date : nil)
234 due_date || (fixed_version ? fixed_version.effective_date : nil)
237 end
235 end
238
236
239 def duration
237 def duration
240 (start_date && due_date) ? due_date - start_date : 0
238 (start_date && due_date) ? due_date - start_date : 0
241 end
239 end
242
240
243 def soonest_start
241 def soonest_start
244 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
242 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
245 end
243 end
246
244
247 def self.visible_by(usr)
245 def self.visible_by(usr)
248 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
246 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
249 yield
247 yield
250 end
248 end
251 end
249 end
252
250
253 def to_s
251 def to_s
254 "#{tracker} ##{id}: #{subject}"
252 "#{tracker} ##{id}: #{subject}"
255 end
253 end
256 end
254 end
@@ -1,262 +1,257
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :custom_values, :dependent => :delete_all, :as => :customized
26 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
27 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
28 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
29 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
30 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
31 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
32 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
33 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
34 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
35 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
36 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
37 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
38 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
39 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
40 # Custom field for the project issues
39 # Custom field for the project issues
41 has_and_belongs_to_many :custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
42 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
43 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
44 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
45 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
46
45
47 acts_as_tree :order => "name", :counter_cache => true
46 acts_as_tree :order => "name", :counter_cache => true
48
47
48 acts_as_customizable
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
52
52
53 attr_protected :status, :enabled_module_names
53 attr_protected :status, :enabled_module_names
54
54
55 validates_presence_of :name, :identifier
55 validates_presence_of :name, :identifier
56 validates_uniqueness_of :name, :identifier
56 validates_uniqueness_of :name, :identifier
57 validates_associated :custom_values, :on => :update
58 validates_associated :repository, :wiki
57 validates_associated :repository, :wiki
59 validates_length_of :name, :maximum => 30
58 validates_length_of :name, :maximum => 30
60 validates_length_of :homepage, :maximum => 255
59 validates_length_of :homepage, :maximum => 255
61 validates_length_of :identifier, :in => 3..20
60 validates_length_of :identifier, :in => 3..20
62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
61 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
63
62
64 before_destroy :delete_all_members
63 before_destroy :delete_all_members
65
64
66 def identifier=(identifier)
65 def identifier=(identifier)
67 super unless identifier_frozen?
66 super unless identifier_frozen?
68 end
67 end
69
68
70 def identifier_frozen?
69 def identifier_frozen?
71 errors[:identifier].nil? && !(new_record? || identifier.blank?)
70 errors[:identifier].nil? && !(new_record? || identifier.blank?)
72 end
71 end
73
72
74 def issues_with_subprojects(include_subprojects=false)
73 def issues_with_subprojects(include_subprojects=false)
75 conditions = nil
74 conditions = nil
76 if include_subprojects
75 if include_subprojects
77 ids = [id] + child_ids
76 ids = [id] + child_ids
78 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
77 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
79 end
78 end
80 conditions ||= ["#{Project.table_name}.id = ?", id]
79 conditions ||= ["#{Project.table_name}.id = ?", id]
81 # Quick and dirty fix for Rails 2 compatibility
80 # Quick and dirty fix for Rails 2 compatibility
82 Issue.send(:with_scope, :find => { :conditions => conditions }) do
81 Issue.send(:with_scope, :find => { :conditions => conditions }) do
83 Version.send(:with_scope, :find => { :conditions => conditions }) do
82 Version.send(:with_scope, :find => { :conditions => conditions }) do
84 yield
83 yield
85 end
84 end
86 end
85 end
87 end
86 end
88
87
89 # returns latest created projects
88 # returns latest created projects
90 # non public projects will be returned only if user is a member of those
89 # non public projects will be returned only if user is a member of those
91 def self.latest(user=nil, count=5)
90 def self.latest(user=nil, count=5)
92 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
91 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
93 end
92 end
94
93
95 def self.visible_by(user=nil)
94 def self.visible_by(user=nil)
96 user ||= User.current
95 user ||= User.current
97 if user && user.admin?
96 if user && user.admin?
98 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
97 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
99 elsif user && user.memberships.any?
98 elsif user && user.memberships.any?
100 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
99 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
101 else
100 else
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
101 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
103 end
102 end
104 end
103 end
105
104
106 def self.allowed_to_condition(user, permission, options={})
105 def self.allowed_to_condition(user, permission, options={})
107 statements = []
106 statements = []
108 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
109 if options[:project]
108 if options[:project]
110 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
109 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
111 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
110 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
112 base_statement = "(#{project_statement}) AND (#{base_statement})"
111 base_statement = "(#{project_statement}) AND (#{base_statement})"
113 end
112 end
114 if user.admin?
113 if user.admin?
115 # no restriction
114 # no restriction
116 elsif user.logged?
115 elsif user.logged?
117 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
116 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
118 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
117 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
119 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
118 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
120 elsif Role.anonymous.allowed_to?(permission)
119 elsif Role.anonymous.allowed_to?(permission)
121 # anonymous user allowed on public project
120 # anonymous user allowed on public project
122 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
121 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
123 else
122 else
124 # anonymous user is not authorized
123 # anonymous user is not authorized
125 statements << "1=0"
124 statements << "1=0"
126 end
125 end
127 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
126 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
128 end
127 end
129
128
130 def project_condition(with_subprojects)
129 def project_condition(with_subprojects)
131 cond = "#{Project.table_name}.id = #{id}"
130 cond = "#{Project.table_name}.id = #{id}"
132 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
131 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
133 cond
132 cond
134 end
133 end
135
134
136 def self.find(*args)
135 def self.find(*args)
137 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
136 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
138 project = find_by_identifier(*args)
137 project = find_by_identifier(*args)
139 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
138 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
140 project
139 project
141 else
140 else
142 super
141 super
143 end
142 end
144 end
143 end
145
144
146 def to_param
145 def to_param
147 # id is used for projects with a numeric identifier (compatibility)
146 # id is used for projects with a numeric identifier (compatibility)
148 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
147 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
149 end
148 end
150
149
151 def active?
150 def active?
152 self.status == STATUS_ACTIVE
151 self.status == STATUS_ACTIVE
153 end
152 end
154
153
155 def archive
154 def archive
156 # Archive subprojects if any
155 # Archive subprojects if any
157 children.each do |subproject|
156 children.each do |subproject|
158 subproject.archive
157 subproject.archive
159 end
158 end
160 update_attribute :status, STATUS_ARCHIVED
159 update_attribute :status, STATUS_ARCHIVED
161 end
160 end
162
161
163 def unarchive
162 def unarchive
164 return false if parent && !parent.active?
163 return false if parent && !parent.active?
165 update_attribute :status, STATUS_ACTIVE
164 update_attribute :status, STATUS_ACTIVE
166 end
165 end
167
166
168 def active_children
167 def active_children
169 children.select {|child| child.active?}
168 children.select {|child| child.active?}
170 end
169 end
171
170
172 # Returns an array of the trackers used by the project and its sub projects
171 # Returns an array of the trackers used by the project and its sub projects
173 def rolled_up_trackers
172 def rolled_up_trackers
174 @rolled_up_trackers ||=
173 @rolled_up_trackers ||=
175 Tracker.find(:all, :include => :projects,
174 Tracker.find(:all, :include => :projects,
176 :select => "DISTINCT #{Tracker.table_name}.*",
175 :select => "DISTINCT #{Tracker.table_name}.*",
177 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
176 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
178 :order => "#{Tracker.table_name}.position")
177 :order => "#{Tracker.table_name}.position")
179 end
178 end
180
179
181 # Deletes all project's members
180 # Deletes all project's members
182 def delete_all_members
181 def delete_all_members
183 Member.delete_all(['project_id = ?', id])
182 Member.delete_all(['project_id = ?', id])
184 end
183 end
185
184
186 # Users issues can be assigned to
185 # Users issues can be assigned to
187 def assignable_users
186 def assignable_users
188 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
187 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
189 end
188 end
190
189
191 # Returns the mail adresses of users that should be always notified on project events
190 # Returns the mail adresses of users that should be always notified on project events
192 def recipients
191 def recipients
193 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
192 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
194 end
193 end
195
194
196 # Returns an array of all custom fields enabled for project issues
195 # Returns an array of all custom fields enabled for project issues
197 # (explictly associated custom fields and custom fields enabled for all projects)
196 # (explictly associated custom fields and custom fields enabled for all projects)
198 def custom_fields_for_issues(tracker)
197 def all_issue_custom_fields
199 all_custom_fields.select {|c| tracker.custom_fields.include? c }
198 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq
200 end
201
202 def all_custom_fields
203 @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
204 end
199 end
205
200
206 def project
201 def project
207 self
202 self
208 end
203 end
209
204
210 def <=>(project)
205 def <=>(project)
211 name.downcase <=> project.name.downcase
206 name.downcase <=> project.name.downcase
212 end
207 end
213
208
214 def to_s
209 def to_s
215 name
210 name
216 end
211 end
217
212
218 # Returns a short description of the projects (first lines)
213 # Returns a short description of the projects (first lines)
219 def short_description(length = 255)
214 def short_description(length = 255)
220 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
215 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
221 end
216 end
222
217
223 def allows_to?(action)
218 def allows_to?(action)
224 if action.is_a? Hash
219 if action.is_a? Hash
225 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
220 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
226 else
221 else
227 allowed_permissions.include? action
222 allowed_permissions.include? action
228 end
223 end
229 end
224 end
230
225
231 def module_enabled?(module_name)
226 def module_enabled?(module_name)
232 module_name = module_name.to_s
227 module_name = module_name.to_s
233 enabled_modules.detect {|m| m.name == module_name}
228 enabled_modules.detect {|m| m.name == module_name}
234 end
229 end
235
230
236 def enabled_module_names=(module_names)
231 def enabled_module_names=(module_names)
237 enabled_modules.clear
232 enabled_modules.clear
238 module_names = [] unless module_names && module_names.is_a?(Array)
233 module_names = [] unless module_names && module_names.is_a?(Array)
239 module_names.each do |name|
234 module_names.each do |name|
240 enabled_modules << EnabledModule.new(:name => name.to_s)
235 enabled_modules << EnabledModule.new(:name => name.to_s)
241 end
236 end
242 end
237 end
243
238
244 protected
239 protected
245 def validate
240 def validate
246 errors.add(parent_id, " must be a root project") if parent and parent.parent
241 errors.add(parent_id, " must be a root project") if parent and parent.parent
247 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
242 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
248 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
243 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
249 end
244 end
250
245
251 private
246 private
252 def allowed_permissions
247 def allowed_permissions
253 @allowed_permissions ||= begin
248 @allowed_permissions ||= begin
254 module_names = enabled_modules.collect {|m| m.name}
249 module_names = enabled_modules.collect {|m| m.name}
255 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
250 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
256 end
251 end
257 end
252 end
258
253
259 def allowed_actions
254 def allowed_actions
260 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
255 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
261 end
256 end
262 end
257 end
@@ -1,382 +1,382
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = false
38 self.sortable = false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=" ] }
91 :integer => [ "=", ">=", "<=" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
100 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
110 ]
111 cattr_reader :available_columns
111 cattr_reader :available_columns
112
112
113 def initialize(attributes = nil)
113 def initialize(attributes = nil)
114 super attributes
114 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
116 set_language_if_valid(User.current.language)
117 end
117 end
118
118
119 def after_initialize
119 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
120 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
121 @is_for_all = project.nil?
122 end
122 end
123
123
124 def validate
124 def validate
125 filters.each_key do |field|
125 filters.each_key do |field|
126 errors.add label_for(field), :activerecord_error_blank unless
126 errors.add label_for(field), :activerecord_error_blank unless
127 # filter requires one or more values
127 # filter requires one or more values
128 (values_for(field) and !values_for(field).first.blank?) or
128 (values_for(field) and !values_for(field).first.blank?) or
129 # filter doesn't require any value
129 # filter doesn't require any value
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 end if filters
131 end if filters
132 end
132 end
133
133
134 def editable_by?(user)
134 def editable_by?(user)
135 return false unless user
135 return false unless user
136 # Admin can edit them all and regular users can edit their private queries
136 # Admin can edit them all and regular users can edit their private queries
137 return true if user.admin? || (!is_public && self.user_id == user.id)
137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 end
140 end
141
141
142 def available_filters
142 def available_filters
143 return @available_filters if @available_filters
143 return @available_filters if @available_filters
144
144
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146
146
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "subject" => { :type => :text, :order => 8 },
150 "subject" => { :type => :text, :order => 8 },
151 "created_on" => { :type => :date_past, :order => 9 },
151 "created_on" => { :type => :date_past, :order => 9 },
152 "updated_on" => { :type => :date_past, :order => 10 },
152 "updated_on" => { :type => :date_past, :order => 10 },
153 "start_date" => { :type => :date, :order => 11 },
153 "start_date" => { :type => :date, :order => 11 },
154 "due_date" => { :type => :date, :order => 12 },
154 "due_date" => { :type => :date, :order => 12 },
155 "done_ratio" => { :type => :integer, :order => 13 }}
155 "done_ratio" => { :type => :integer, :order => 13 }}
156
156
157 user_values = []
157 user_values = []
158 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
158 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
159 if project
159 if project
160 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
160 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
161 else
161 else
162 # members of the user's projects
162 # members of the user's projects
163 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
163 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
164 end
164 end
165 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
165 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
166 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
166 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
167
167
168 if project
168 if project
169 # project specific filters
169 # project specific filters
170 unless @project.issue_categories.empty?
170 unless @project.issue_categories.empty?
171 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
171 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
172 end
172 end
173 unless @project.versions.empty?
173 unless @project.versions.empty?
174 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
174 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
175 end
175 end
176 unless @project.active_children.empty?
176 unless @project.active_children.empty?
177 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
177 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
178 end
178 end
179 add_custom_fields_filters(@project.all_custom_fields)
179 add_custom_fields_filters(@project.all_issue_custom_fields)
180 else
180 else
181 # global filters for cross project issue list
181 # global filters for cross project issue list
182 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
182 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
183 end
183 end
184 @available_filters
184 @available_filters
185 end
185 end
186
186
187 def add_filter(field, operator, values)
187 def add_filter(field, operator, values)
188 # values must be an array
188 # values must be an array
189 return unless values and values.is_a? Array # and !values.first.empty?
189 return unless values and values.is_a? Array # and !values.first.empty?
190 # check if field is defined as an available filter
190 # check if field is defined as an available filter
191 if available_filters.has_key? field
191 if available_filters.has_key? field
192 filter_options = available_filters[field]
192 filter_options = available_filters[field]
193 # check if operator is allowed for that filter
193 # check if operator is allowed for that filter
194 #if @@operators_by_filter_type[filter_options[:type]].include? operator
194 #if @@operators_by_filter_type[filter_options[:type]].include? operator
195 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
195 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
196 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
196 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
197 #end
197 #end
198 filters[field] = {:operator => operator, :values => values }
198 filters[field] = {:operator => operator, :values => values }
199 end
199 end
200 end
200 end
201
201
202 def add_short_filter(field, expression)
202 def add_short_filter(field, expression)
203 return unless expression
203 return unless expression
204 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
204 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
205 add_filter field, (parms[0] || "="), [parms[1] || ""]
205 add_filter field, (parms[0] || "="), [parms[1] || ""]
206 end
206 end
207
207
208 def has_filter?(field)
208 def has_filter?(field)
209 filters and filters[field]
209 filters and filters[field]
210 end
210 end
211
211
212 def operator_for(field)
212 def operator_for(field)
213 has_filter?(field) ? filters[field][:operator] : nil
213 has_filter?(field) ? filters[field][:operator] : nil
214 end
214 end
215
215
216 def values_for(field)
216 def values_for(field)
217 has_filter?(field) ? filters[field][:values] : nil
217 has_filter?(field) ? filters[field][:values] : nil
218 end
218 end
219
219
220 def label_for(field)
220 def label_for(field)
221 label = available_filters[field][:name] if available_filters.has_key?(field)
221 label = available_filters[field][:name] if available_filters.has_key?(field)
222 label ||= field.gsub(/\_id$/, "")
222 label ||= field.gsub(/\_id$/, "")
223 end
223 end
224
224
225 def available_columns
225 def available_columns
226 return @available_columns if @available_columns
226 return @available_columns if @available_columns
227 @available_columns = Query.available_columns
227 @available_columns = Query.available_columns
228 @available_columns += (project ?
228 @available_columns += (project ?
229 project.all_custom_fields :
229 project.all_issue_custom_fields :
230 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
230 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
231 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
231 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
232 end
232 end
233
233
234 def columns
234 def columns
235 if has_default_columns?
235 if has_default_columns?
236 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
236 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
237 else
237 else
238 # preserve the column_names order
238 # preserve the column_names order
239 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
239 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
240 end
240 end
241 end
241 end
242
242
243 def column_names=(names)
243 def column_names=(names)
244 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
244 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
245 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
245 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
246 write_attribute(:column_names, names)
246 write_attribute(:column_names, names)
247 end
247 end
248
248
249 def has_column?(column)
249 def has_column?(column)
250 column_names && column_names.include?(column.name)
250 column_names && column_names.include?(column.name)
251 end
251 end
252
252
253 def has_default_columns?
253 def has_default_columns?
254 column_names.nil? || column_names.empty?
254 column_names.nil? || column_names.empty?
255 end
255 end
256
256
257 def statement
257 def statement
258 # project/subprojects clause
258 # project/subprojects clause
259 project_clauses = []
259 project_clauses = []
260 if project && !@project.active_children.empty?
260 if project && !@project.active_children.empty?
261 ids = [project.id]
261 ids = [project.id]
262 if has_filter?("subproject_id")
262 if has_filter?("subproject_id")
263 case operator_for("subproject_id")
263 case operator_for("subproject_id")
264 when '='
264 when '='
265 # include the selected subprojects
265 # include the selected subprojects
266 ids += values_for("subproject_id").each(&:to_i)
266 ids += values_for("subproject_id").each(&:to_i)
267 when '!*'
267 when '!*'
268 # main project only
268 # main project only
269 else
269 else
270 # all subprojects
270 # all subprojects
271 ids += project.child_ids
271 ids += project.child_ids
272 end
272 end
273 elsif Setting.display_subprojects_issues?
273 elsif Setting.display_subprojects_issues?
274 ids += project.child_ids
274 ids += project.child_ids
275 end
275 end
276 project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
276 project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
277 elsif project
277 elsif project
278 project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
278 project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
279 end
279 end
280 project_clauses << Project.visible_by(User.current)
280 project_clauses << Project.visible_by(User.current)
281
281
282 # filters clauses
282 # filters clauses
283 filters_clauses = []
283 filters_clauses = []
284 filters.each_key do |field|
284 filters.each_key do |field|
285 next if field == "subproject_id"
285 next if field == "subproject_id"
286 v = values_for(field).clone
286 v = values_for(field).clone
287 next unless v and !v.empty?
287 next unless v and !v.empty?
288
288
289 sql = ''
289 sql = ''
290 is_custom_filter = false
290 is_custom_filter = false
291 if field =~ /^cf_(\d+)$/
291 if field =~ /^cf_(\d+)$/
292 # custom field
292 # custom field
293 db_table = CustomValue.table_name
293 db_table = CustomValue.table_name
294 db_field = 'value'
294 db_field = 'value'
295 is_custom_filter = true
295 is_custom_filter = true
296 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
296 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
297 else
297 else
298 # regular field
298 # regular field
299 db_table = Issue.table_name
299 db_table = Issue.table_name
300 db_field = field
300 db_field = field
301 sql << '('
301 sql << '('
302 end
302 end
303
303
304 # "me" value subsitution
304 # "me" value subsitution
305 if %w(assigned_to_id author_id).include?(field)
305 if %w(assigned_to_id author_id).include?(field)
306 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
306 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
307 end
307 end
308
308
309 case operator_for field
309 case operator_for field
310 when "="
310 when "="
311 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
311 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
312 when "!"
312 when "!"
313 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
313 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
314 when "!*"
314 when "!*"
315 sql = sql + "#{db_table}.#{db_field} IS NULL"
315 sql = sql + "#{db_table}.#{db_field} IS NULL"
316 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
316 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
317 when "*"
317 when "*"
318 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
318 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
319 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
319 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
320 when ">="
320 when ">="
321 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
321 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
322 when "<="
322 when "<="
323 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
323 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
324 when "o"
324 when "o"
325 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
325 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
326 when "c"
326 when "c"
327 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
327 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
328 when ">t-"
328 when ">t-"
329 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
329 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
330 when "<t-"
330 when "<t-"
331 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
331 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
332 when "t-"
332 when "t-"
333 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
333 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
334 when ">t+"
334 when ">t+"
335 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
335 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
336 when "<t+"
336 when "<t+"
337 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
337 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
338 when "t+"
338 when "t+"
339 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
339 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
340 when "t"
340 when "t"
341 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
341 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
342 when "w"
342 when "w"
343 from = l(:general_first_day_of_week) == '7' ?
343 from = l(:general_first_day_of_week) == '7' ?
344 # week starts on sunday
344 # week starts on sunday
345 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
345 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
346 # week starts on monday (Rails default)
346 # week starts on monday (Rails default)
347 Time.now.at_beginning_of_week
347 Time.now.at_beginning_of_week
348 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
348 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
349 when "~"
349 when "~"
350 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
350 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
351 when "!~"
351 when "!~"
352 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
352 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
353 end
353 end
354 sql << ')'
354 sql << ')'
355 filters_clauses << sql
355 filters_clauses << sql
356 end if filters and valid?
356 end if filters and valid?
357
357
358 (project_clauses + filters_clauses).join(' AND ')
358 (project_clauses + filters_clauses).join(' AND ')
359 end
359 end
360
360
361 private
361 private
362
362
363 def add_custom_fields_filters(custom_fields)
363 def add_custom_fields_filters(custom_fields)
364 @available_filters ||= {}
364 @available_filters ||= {}
365
365
366 custom_fields.select(&:is_filter?).each do |field|
366 custom_fields.select(&:is_filter?).each do |field|
367 case field.field_format
367 case field.field_format
368 when "text"
368 when "text"
369 options = { :type => :text, :order => 20 }
369 options = { :type => :text, :order => 20 }
370 when "list"
370 when "list"
371 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
371 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
372 when "date"
372 when "date"
373 options = { :type => :date, :order => 20 }
373 options = { :type => :date, :order => 20 }
374 when "bool"
374 when "bool"
375 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
375 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
376 else
376 else
377 options = { :type => :string, :order => 20 }
377 options = { :type => :string, :order => 20 }
378 end
378 end
379 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
379 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
380 end
380 end
381 end
381 end
382 end
382 end
@@ -1,290 +1,294
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < ActiveRecord::Base
20 class User < ActiveRecord::Base
21
21
22 class OnTheFlyCreationFailure < Exception; end
22 class OnTheFlyCreationFailure < Exception; end
23
23
24 # Account statuses
24 # Account statuses
25 STATUS_ANONYMOUS = 0
25 STATUS_ANONYMOUS = 0
26 STATUS_ACTIVE = 1
26 STATUS_ACTIVE = 1
27 STATUS_REGISTERED = 2
27 STATUS_REGISTERED = 2
28 STATUS_LOCKED = 3
28 STATUS_LOCKED = 3
29
29
30 USER_FORMATS = {
30 USER_FORMATS = {
31 :firstname_lastname => '#{firstname} #{lastname}',
31 :firstname_lastname => '#{firstname} #{lastname}',
32 :firstname => '#{firstname}',
32 :firstname => '#{firstname}',
33 :lastname_firstname => '#{lastname} #{firstname}',
33 :lastname_firstname => '#{lastname} #{firstname}',
34 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 :lastname_coma_firstname => '#{lastname}, #{firstname}',
35 :username => '#{login}'
35 :username => '#{login}'
36 }
36 }
37
37
38 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
38 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
39 has_many :projects, :through => :memberships
39 has_many :projects, :through => :memberships
40 has_many :custom_values, :dependent => :delete_all, :as => :customized
41 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
42 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
43 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
44 belongs_to :auth_source
43 belongs_to :auth_source
45
44
45 acts_as_customizable
46
46 attr_accessor :password, :password_confirmation
47 attr_accessor :password, :password_confirmation
47 attr_accessor :last_before_login_on
48 attr_accessor :last_before_login_on
48 # Prevents unauthorized assignments
49 # Prevents unauthorized assignments
49 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
50 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
50
51
51 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
52 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
52 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
53 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
53 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
54 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
54 # Login must contain lettres, numbers, underscores only
55 # Login must contain lettres, numbers, underscores only
55 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
56 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
56 validates_length_of :login, :maximum => 30
57 validates_length_of :login, :maximum => 30
57 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
58 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
58 validates_length_of :firstname, :lastname, :maximum => 30
59 validates_length_of :firstname, :lastname, :maximum => 30
59 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
60 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
60 validates_length_of :mail, :maximum => 60, :allow_nil => true
61 validates_length_of :mail, :maximum => 60, :allow_nil => true
61 validates_length_of :password, :minimum => 4, :allow_nil => true
62 validates_length_of :password, :minimum => 4, :allow_nil => true
62 validates_confirmation_of :password, :allow_nil => true
63 validates_confirmation_of :password, :allow_nil => true
63 validates_associated :custom_values, :on => :update
64
64
65 def before_create
65 def before_create
66 self.mail_notification = false
66 self.mail_notification = false
67 true
67 true
68 end
68 end
69
69
70 def before_save
70 def before_save
71 # update hashed_password if password was set
71 # update hashed_password if password was set
72 self.hashed_password = User.hash_password(self.password) if self.password
72 self.hashed_password = User.hash_password(self.password) if self.password
73 end
73 end
74
74
75 def self.active
75 def self.active
76 with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
76 with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
77 yield
77 yield
78 end
78 end
79 end
79 end
80
80
81 def self.find_active(*args)
81 def self.find_active(*args)
82 active do
82 active do
83 find(*args)
83 find(*args)
84 end
84 end
85 end
85 end
86
86
87 # Returns the user that matches provided login and password, or nil
87 # Returns the user that matches provided login and password, or nil
88 def self.try_to_login(login, password)
88 def self.try_to_login(login, password)
89 # Make sure no one can sign in with an empty password
89 # Make sure no one can sign in with an empty password
90 return nil if password.to_s.empty?
90 return nil if password.to_s.empty?
91 user = find(:first, :conditions => ["login=?", login])
91 user = find(:first, :conditions => ["login=?", login])
92 if user
92 if user
93 # user is already in local database
93 # user is already in local database
94 return nil if !user.active?
94 return nil if !user.active?
95 if user.auth_source
95 if user.auth_source
96 # user has an external authentication method
96 # user has an external authentication method
97 return nil unless user.auth_source.authenticate(login, password)
97 return nil unless user.auth_source.authenticate(login, password)
98 else
98 else
99 # authentication with local password
99 # authentication with local password
100 return nil unless User.hash_password(password) == user.hashed_password
100 return nil unless User.hash_password(password) == user.hashed_password
101 end
101 end
102 else
102 else
103 # user is not yet registered, try to authenticate with available sources
103 # user is not yet registered, try to authenticate with available sources
104 attrs = AuthSource.authenticate(login, password)
104 attrs = AuthSource.authenticate(login, password)
105 if attrs
105 if attrs
106 onthefly = new(*attrs)
106 onthefly = new(*attrs)
107 onthefly.login = login
107 onthefly.login = login
108 onthefly.language = Setting.default_language
108 onthefly.language = Setting.default_language
109 if onthefly.save
109 if onthefly.save
110 user = find(:first, :conditions => ["login=?", login])
110 user = find(:first, :conditions => ["login=?", login])
111 logger.info("User '#{user.login}' created from the LDAP") if logger
111 logger.info("User '#{user.login}' created from the LDAP") if logger
112 else
112 else
113 logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
113 logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
114 raise OnTheFlyCreationFailure.new
114 raise OnTheFlyCreationFailure.new
115 end
115 end
116 end
116 end
117 end
117 end
118 user.update_attribute(:last_login_on, Time.now) if user
118 user.update_attribute(:last_login_on, Time.now) if user
119 user
119 user
120 rescue => text
120 rescue => text
121 raise text
121 raise text
122 end
122 end
123
123
124 # Return user's full name for display
124 # Return user's full name for display
125 def name(formatter = nil)
125 def name(formatter = nil)
126 f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
126 f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
127 eval '"' + f + '"'
127 eval '"' + f + '"'
128 end
128 end
129
129
130 def active?
130 def active?
131 self.status == STATUS_ACTIVE
131 self.status == STATUS_ACTIVE
132 end
132 end
133
133
134 def registered?
134 def registered?
135 self.status == STATUS_REGISTERED
135 self.status == STATUS_REGISTERED
136 end
136 end
137
137
138 def locked?
138 def locked?
139 self.status == STATUS_LOCKED
139 self.status == STATUS_LOCKED
140 end
140 end
141
141
142 def check_password?(clear_password)
142 def check_password?(clear_password)
143 User.hash_password(clear_password) == self.hashed_password
143 User.hash_password(clear_password) == self.hashed_password
144 end
144 end
145
145
146 def pref
146 def pref
147 self.preference ||= UserPreference.new(:user => self)
147 self.preference ||= UserPreference.new(:user => self)
148 end
148 end
149
149
150 def time_zone
150 def time_zone
151 self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
151 self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
152 end
152 end
153
153
154 def wants_comments_in_reverse_order?
154 def wants_comments_in_reverse_order?
155 self.pref[:comments_sorting] == 'desc'
155 self.pref[:comments_sorting] == 'desc'
156 end
156 end
157
157
158 # Return user's RSS key (a 40 chars long string), used to access feeds
158 # Return user's RSS key (a 40 chars long string), used to access feeds
159 def rss_key
159 def rss_key
160 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
160 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
161 token.value
161 token.value
162 end
162 end
163
163
164 # Return an array of project ids for which the user has explicitly turned mail notifications on
164 # Return an array of project ids for which the user has explicitly turned mail notifications on
165 def notified_projects_ids
165 def notified_projects_ids
166 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
166 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
167 end
167 end
168
168
169 def notified_project_ids=(ids)
169 def notified_project_ids=(ids)
170 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
170 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
171 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
171 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
172 @notified_projects_ids = nil
172 @notified_projects_ids = nil
173 notified_projects_ids
173 notified_projects_ids
174 end
174 end
175
175
176 def self.find_by_rss_key(key)
176 def self.find_by_rss_key(key)
177 token = Token.find_by_value(key)
177 token = Token.find_by_value(key)
178 token && token.user.active? ? token.user : nil
178 token && token.user.active? ? token.user : nil
179 end
179 end
180
180
181 def self.find_by_autologin_key(key)
181 def self.find_by_autologin_key(key)
182 token = Token.find_by_action_and_value('autologin', key)
182 token = Token.find_by_action_and_value('autologin', key)
183 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
183 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
184 end
184 end
185
185
186 def <=>(user)
186 def <=>(user)
187 if user.nil?
187 if user.nil?
188 -1
188 -1
189 elsif lastname.to_s.downcase == user.lastname.to_s.downcase
189 elsif lastname.to_s.downcase == user.lastname.to_s.downcase
190 firstname.to_s.downcase <=> user.firstname.to_s.downcase
190 firstname.to_s.downcase <=> user.firstname.to_s.downcase
191 else
191 else
192 lastname.to_s.downcase <=> user.lastname.to_s.downcase
192 lastname.to_s.downcase <=> user.lastname.to_s.downcase
193 end
193 end
194 end
194 end
195
195
196 def to_s
196 def to_s
197 name
197 name
198 end
198 end
199
199
200 def logged?
200 def logged?
201 true
201 true
202 end
202 end
203
203
204 # Return user's role for project
204 # Return user's role for project
205 def role_for_project(project)
205 def role_for_project(project)
206 # No role on archived projects
206 # No role on archived projects
207 return nil unless project && project.active?
207 return nil unless project && project.active?
208 if logged?
208 if logged?
209 # Find project membership
209 # Find project membership
210 membership = memberships.detect {|m| m.project_id == project.id}
210 membership = memberships.detect {|m| m.project_id == project.id}
211 if membership
211 if membership
212 membership.role
212 membership.role
213 else
213 else
214 @role_non_member ||= Role.non_member
214 @role_non_member ||= Role.non_member
215 end
215 end
216 else
216 else
217 @role_anonymous ||= Role.anonymous
217 @role_anonymous ||= Role.anonymous
218 end
218 end
219 end
219 end
220
220
221 # Return true if the user is a member of project
221 # Return true if the user is a member of project
222 def member_of?(project)
222 def member_of?(project)
223 role_for_project(project).member?
223 role_for_project(project).member?
224 end
224 end
225
225
226 # Return true if the user is allowed to do the specified action on project
226 # Return true if the user is allowed to do the specified action on project
227 # action can be:
227 # action can be:
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
229 # * a permission Symbol (eg. :edit_project)
229 # * a permission Symbol (eg. :edit_project)
230 def allowed_to?(action, project, options={})
230 def allowed_to?(action, project, options={})
231 if project
231 if project
232 # No action allowed on archived projects
232 # No action allowed on archived projects
233 return false unless project.active?
233 return false unless project.active?
234 # No action allowed on disabled modules
234 # No action allowed on disabled modules
235 return false unless project.allows_to?(action)
235 return false unless project.allows_to?(action)
236 # Admin users are authorized for anything else
236 # Admin users are authorized for anything else
237 return true if admin?
237 return true if admin?
238
238
239 role = role_for_project(project)
239 role = role_for_project(project)
240 return false unless role
240 return false unless role
241 role.allowed_to?(action) && (project.is_public? || role.member?)
241 role.allowed_to?(action) && (project.is_public? || role.member?)
242
242
243 elsif options[:global]
243 elsif options[:global]
244 # authorize if user has at least one role that has this permission
244 # authorize if user has at least one role that has this permission
245 roles = memberships.collect {|m| m.role}.uniq
245 roles = memberships.collect {|m| m.role}.uniq
246 roles.detect {|r| r.allowed_to?(action)}
246 roles.detect {|r| r.allowed_to?(action)}
247 else
247 else
248 false
248 false
249 end
249 end
250 end
250 end
251
251
252 def self.current=(user)
252 def self.current=(user)
253 @current_user = user
253 @current_user = user
254 end
254 end
255
255
256 def self.current
256 def self.current
257 @current_user ||= User.anonymous
257 @current_user ||= User.anonymous
258 end
258 end
259
259
260 def self.anonymous
260 def self.anonymous
261 anonymous_user = AnonymousUser.find(:first)
261 anonymous_user = AnonymousUser.find(:first)
262 if anonymous_user.nil?
262 if anonymous_user.nil?
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
265 end
265 end
266 anonymous_user
266 anonymous_user
267 end
267 end
268
268
269 private
269 private
270 # Return password digest
270 # Return password digest
271 def self.hash_password(clear_password)
271 def self.hash_password(clear_password)
272 Digest::SHA1.hexdigest(clear_password || "")
272 Digest::SHA1.hexdigest(clear_password || "")
273 end
273 end
274 end
274 end
275
275
276 class AnonymousUser < User
276 class AnonymousUser < User
277
277
278 def validate_on_create
278 def validate_on_create
279 # There should be only one AnonymousUser in the database
279 # There should be only one AnonymousUser in the database
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
281 end
281 end
282
282
283 def available_custom_fields
284 []
285 end
286
283 # Overrides a few properties
287 # Overrides a few properties
284 def logged?; false end
288 def logged?; false end
285 def admin; false end
289 def admin; false end
286 def name; 'Anonymous' end
290 def name; 'Anonymous' end
287 def mail; nil end
291 def mail; nil end
288 def time_zone; nil end
292 def time_zone; nil end
289 def rss_key; nil end
293 def rss_key; nil end
290 end
294 end
@@ -1,37 +1,37
1 <h2><%=l(:label_register)%></h2>
1 <h2><%=l(:label_register)%></h2>
2
2
3 <% form_tag({:action => 'register'}, :class => "tabular") do %>
3 <% form_tag({:action => 'register'}, :class => "tabular") do %>
4 <%= error_messages_for 'user' %>
4 <%= error_messages_for 'user' %>
5
5
6 <div class="box">
6 <div class="box">
7 <!--[form:user]-->
7 <!--[form:user]-->
8 <p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label>
8 <p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label>
9 <%= text_field 'user', 'login', :size => 25 %></p>
9 <%= text_field 'user', 'login', :size => 25 %></p>
10
10
11 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
11 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
12 <%= password_field_tag 'password', nil, :size => 25 %><br />
12 <%= password_field_tag 'password', nil, :size => 25 %><br />
13 <em><%= l(:text_caracters_minimum, 4) %></em></p>
13 <em><%= l(:text_caracters_minimum, 4) %></em></p>
14
14
15 <p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
15 <p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
16 <%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
16 <%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
17
17
18 <p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label>
18 <p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label>
19 <%= text_field 'user', 'firstname' %></p>
19 <%= text_field 'user', 'firstname' %></p>
20
20
21 <p><label for="user_lastname"><%=l(:field_lastname)%> <span class="required">*</span></label>
21 <p><label for="user_lastname"><%=l(:field_lastname)%> <span class="required">*</span></label>
22 <%= text_field 'user', 'lastname' %></p>
22 <%= text_field 'user', 'lastname' %></p>
23
23
24 <p><label for="user_mail"><%=l(:field_mail)%> <span class="required">*</span></label>
24 <p><label for="user_mail"><%=l(:field_mail)%> <span class="required">*</span></label>
25 <%= text_field 'user', 'mail' %></p>
25 <%= text_field 'user', 'mail' %></p>
26
26
27 <p><label for="user_language"><%=l(:field_language)%></label>
27 <p><label for="user_language"><%=l(:field_language)%></label>
28 <%= select("user", "language", lang_options_for_select) %></p>
28 <%= select("user", "language", lang_options_for_select) %></p>
29
29
30 <% for @custom_value in @custom_values %>
30 <% @user.custom_field_values.each do |value| %>
31 <p><%= custom_field_tag_with_label @custom_value %></p>
31 <p><%= custom_field_tag_with_label :user, value %></p>
32 <% end %>
32 <% end %>
33 <!--[eoform:user]-->
33 <!--[eoform:user]-->
34 </div>
34 </div>
35
35
36 <%= submit_tag l(:button_submit) %>
36 <%= submit_tag l(:button_submit) %>
37 <% end %>
37 <% end %>
@@ -1,11 +1,12
1 <div class="splitcontentleft">
1 <div class="splitcontentleft">
2 <% i = 1 %>
2 <% i = 1 %>
3 <% for @custom_value in values %>
3 <% split_on = @issue.custom_field_values.size / 2 %>
4 <p><%= custom_field_tag_with_label @custom_value %></p>
4 <% @issue.custom_field_values.each do |value| %>
5 <% if i == values.size / 2 %>
5 <p><%= custom_field_tag_with_label :issue, value %></p>
6 <% if i == split_on -%>
6 </div><div class="splitcontentright">
7 </div><div class="splitcontentright">
7 <% end %>
8 <% end -%>
8 <% i += 1 %>
9 <% i += 1 -%>
9 <% end %>
10 <% end -%>
10 </div>
11 </div>
11 <div style="clear:both;"> </div>
12 <div style="clear:both;"> </div>
@@ -1,120 +1,120
1 <div class="contextual">
1 <div class="contextual">
2 <%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %>
2 <%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
8 </div>
8 </div>
9
9
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
11
11
12 <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>">
12 <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>">
13 <h3><%=h @issue.subject %></h3>
13 <h3><%=h @issue.subject %></h3>
14 <p class="author">
14 <p class="author">
15 <%= authoring @issue.created_on, @issue.author %>.
15 <%= authoring @issue.created_on, @issue.author %>.
16 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
16 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
17 </p>
17 </p>
18
18
19 <table width="100%">
19 <table width="100%">
20 <tr>
20 <tr>
21 <td style="width:15%"><b><%=l(:field_status)%>:</b></td><td style="width:35%"><%= @issue.status.name %></td>
21 <td style="width:15%"><b><%=l(:field_status)%>:</b></td><td style="width:35%"><%= @issue.status.name %></td>
22 <td style="width:15%"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
22 <td style="width:15%"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
23 </tr>
23 </tr>
24 <tr>
24 <tr>
25 <td><b><%=l(:field_priority)%>:</b></td><td><%= @issue.priority.name %></td>
25 <td><b><%=l(:field_priority)%>:</b></td><td><%= @issue.priority.name %></td>
26 <td><b><%=l(:field_due_date)%>:</b></td><td><%= format_date(@issue.due_date) %></td>
26 <td><b><%=l(:field_due_date)%>:</b></td><td><%= format_date(@issue.due_date) %></td>
27 </tr>
27 </tr>
28 <tr>
28 <tr>
29 <td><b><%=l(:field_assigned_to)%>:</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
29 <td><b><%=l(:field_assigned_to)%>:</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
30 <td><b><%=l(:field_done_ratio)%>:</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
30 <td><b><%=l(:field_done_ratio)%>:</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
31 </tr>
31 </tr>
32 <tr>
32 <tr>
33 <td><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
33 <td><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
34 <% if User.current.allowed_to?(:view_time_entries, @project) %>
34 <% if User.current.allowed_to?(:view_time_entries, @project) %>
35 <td><b><%=l(:label_spent_time)%>:</b></td>
35 <td><b><%=l(:label_spent_time)%>:</b></td>
36 <td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
36 <td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
37 <% end %>
37 <% end %>
38 </tr>
38 </tr>
39 <tr>
39 <tr>
40 <td><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
40 <td><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
41 <% if @issue.estimated_hours %>
41 <% if @issue.estimated_hours %>
42 <td><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
42 <td><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
43 <% end %>
43 <% end %>
44 </tr>
44 </tr>
45 <tr>
45 <tr>
46 <% n = 0
46 <% n = 0 -%>
47 for custom_value in @custom_values %>
47 <% @issue.custom_values.each do |value| -%>
48 <td valign="top"><b><%= custom_value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(custom_value))) %></td>
48 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
49 <% n = n + 1
49 <% n = n + 1
50 if (n > 1)
50 if (n > 1)
51 n = 0 %>
51 n = 0 %>
52 </tr><tr>
52 </tr><tr>
53 <%end
53 <%end
54 end %>
54 end %>
55 </tr>
55 </tr>
56 </table>
56 </table>
57 <hr />
57 <hr />
58
58
59 <div class="contextual">
59 <div class="contextual">
60 <%= link_to_remote(image_tag('comment.png'),
60 <%= link_to_remote(image_tag('comment.png'),
61 { :url => {:controller => 'issues', :action => 'reply', :id => @issue} },
61 { :url => {:controller => 'issues', :action => 'reply', :id => @issue} },
62 :title => l(:button_reply)) if authorize_for('issues', 'edit') %>
62 :title => l(:button_reply)) if authorize_for('issues', 'edit') %>
63 </div>
63 </div>
64
64
65 <p><strong><%=l(:field_description)%></strong></p>
65 <p><strong><%=l(:field_description)%></strong></p>
66 <div class="wiki">
66 <div class="wiki">
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
68 </div>
68 </div>
69
69
70 <% if @issue.attachments.any? %>
70 <% if @issue.attachments.any? %>
71 <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %>
71 <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %>
72 <% end %>
72 <% end %>
73
73
74 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
74 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
75 <hr />
75 <hr />
76 <div id="relations">
76 <div id="relations">
77 <%= render :partial => 'relations' %>
77 <%= render :partial => 'relations' %>
78 </div>
78 </div>
79 <% end %>
79 <% end %>
80
80
81 </div>
81 </div>
82
82
83 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
83 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
84 <div id="issue-changesets">
84 <div id="issue-changesets">
85 <h3><%=l(:label_associated_revisions)%></h3>
85 <h3><%=l(:label_associated_revisions)%></h3>
86 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
86 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
87 </div>
87 </div>
88 <% end %>
88 <% end %>
89
89
90 <% if @journals.any? %>
90 <% if @journals.any? %>
91 <div id="history">
91 <div id="history">
92 <h3><%=l(:label_history)%></h3>
92 <h3><%=l(:label_history)%></h3>
93 <%= render :partial => 'history', :locals => { :journals => @journals } %>
93 <%= render :partial => 'history', :locals => { :journals => @journals } %>
94 </div>
94 </div>
95 <% end %>
95 <% end %>
96 <div style="clear: both;"></div>
96 <div style="clear: both;"></div>
97
97
98 <% if authorize_for('issues', 'edit') %>
98 <% if authorize_for('issues', 'edit') %>
99 <div id="update" style="display:none;">
99 <div id="update" style="display:none;">
100 <h3><%= l(:button_update) %></h3>
100 <h3><%= l(:button_update) %></h3>
101 <%= render :partial => 'edit' %>
101 <%= render :partial => 'edit' %>
102 </div>
102 </div>
103 <% end %>
103 <% end %>
104
104
105 <p class="other-formats">
105 <p class="other-formats">
106 <%= l(:label_export_to) %>
106 <%= l(:label_export_to) %>
107 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
107 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
108 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
108 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
109 </p>
109 </p>
110
110
111 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
111 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
112
112
113 <% content_for :sidebar do %>
113 <% content_for :sidebar do %>
114 <%= render :partial => 'issues/sidebar' %>
114 <%= render :partial => 'issues/sidebar' %>
115 <% end %>
115 <% end %>
116
116
117 <% content_for :header_tags do %>
117 <% content_for :header_tags do %>
118 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
118 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
119 <%= stylesheet_link_tag 'scm' %>
119 <%= stylesheet_link_tag 'scm' %>
120 <% end %>
120 <% end %>
@@ -1,48 +1,48
1 <%= error_messages_for 'project' %>
1 <%= error_messages_for 'project' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:project]-->
4 <!--[form:project]-->
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
6
6
7 <% if User.current.admin? and !@root_projects.empty? %>
7 <% if User.current.admin? and !@root_projects.empty? %>
8 <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
8 <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
9 <% end %>
9 <% end %>
10
10
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
14 <br /><em><%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %></em>
14 <br /><em><%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %></em>
15 <% end %></p>
15 <% end %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
17 <p><%= f.check_box :is_public %></p>
17 <p><%= f.check_box :is_public %></p>
18 <%= wikitoolbar_for 'project_description' %>
18 <%= wikitoolbar_for 'project_description' %>
19
19
20 <% for @custom_value in @custom_values %>
20 <% @project.custom_field_values.each do |value| %>
21 <p><%= custom_field_tag_with_label @custom_value %></p>
21 <p><%= custom_field_tag_with_label :project, value %></p>
22 <% end %>
22 <% end %>
23 </div>
23 </div>
24
24
25 <% unless @trackers.empty? %>
25 <% unless @trackers.empty? %>
26 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
26 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
27 <% @trackers.each do |tracker| %>
27 <% @trackers.each do |tracker| %>
28 <label class="floating">
28 <label class="floating">
29 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
29 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
30 <%= tracker %>
30 <%= tracker %>
31 </label>
31 </label>
32 <% end %>
32 <% end %>
33 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
33 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
34 </fieldset>
34 </fieldset>
35 <% end %>
35 <% end %>
36
36
37 <% unless @custom_fields.empty? %>
37 <% unless @issue_custom_fields.empty? %>
38 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
38 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
39 <% for custom_field in @custom_fields %>
39 <% @issue_custom_fields.each do |custom_field| %>
40 <label class="floating">
40 <label class="floating">
41 <%= check_box_tag 'project[custom_field_ids][]', custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
41 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
42 <%= custom_field.name %>
42 <%= custom_field.name %>
43 </label>
43 </label>
44 <% end %>
44 <% end %>
45 <%= hidden_field_tag 'project[custom_field_ids][]', '' %>
45 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
46 </fieldset>
46 </fieldset>
47 <% end %>
47 <% end %>
48 <!--[eoform:project]-->
48 <!--[eoform:project]-->
@@ -1,80 +1,80
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
9 <% end %>
9 <% end %>
10 <% if @project.parent %>
10 <% if @project.parent %>
11 <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
11 <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
12 <% end %>
12 <% end %>
13 <% for custom_value in @custom_values %>
13 <% @project.custom_values.each do |custom_value| %>
14 <% if !custom_value.value.empty? %>
14 <% if !custom_value.value.empty? %>
15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
16 <% end %>
16 <% end %>
17 <% end %>
17 <% end %>
18 </ul>
18 </ul>
19
19
20 <% if User.current.allowed_to?(:view_issues, @project) %>
20 <% if User.current.allowed_to?(:view_issues, @project) %>
21 <div class="box">
21 <div class="box">
22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
23 <ul>
23 <ul>
24 <% for tracker in @trackers %>
24 <% for tracker in @trackers %>
25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
26 :set_filter => 1,
26 :set_filter => 1,
27 "tracker_id" => tracker.id %>:
27 "tracker_id" => tracker.id %>:
28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
30 <% end %>
30 <% end %>
31 </ul>
31 </ul>
32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
33 </div>
33 </div>
34 <% end %>
34 <% end %>
35 </div>
35 </div>
36
36
37 <div class="splitcontentright">
37 <div class="splitcontentright">
38 <% if @members_by_role.any? %>
38 <% if @members_by_role.any? %>
39 <div class="box">
39 <div class="box">
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 <p><% @members_by_role.keys.sort.each do |role| %>
41 <p><% @members_by_role.keys.sort.each do |role| %>
42 <%= role.name %>:
42 <%= role.name %>:
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 <br />
44 <br />
45 <% end %></p>
45 <% end %></p>
46 </div>
46 </div>
47 <% end %>
47 <% end %>
48
48
49 <% if @news.any? && authorize_for('news', 'index') %>
49 <% if @news.any? && authorize_for('news', 'index') %>
50 <div class="box">
50 <div class="box">
51 <h3><%=l(:label_news_latest)%></h3>
51 <h3><%=l(:label_news_latest)%></h3>
52 <%= render :partial => 'news/news', :collection => @news %>
52 <%= render :partial => 'news/news', :collection => @news %>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 </div>
54 </div>
55 <% end %>
55 <% end %>
56 </div>
56 </div>
57
57
58 <% content_for :sidebar do %>
58 <% content_for :sidebar do %>
59 <% planning_links = []
59 <% planning_links = []
60 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
60 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
61 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project)
61 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project)
62 planning_links.compact!
62 planning_links.compact!
63 unless planning_links.empty? %>
63 unless planning_links.empty? %>
64 <h3><%= l(:label_planning) %></h3>
64 <h3><%= l(:label_planning) %></h3>
65 <p><%= planning_links.join(' | ') %></p>
65 <p><%= planning_links.join(' | ') %></p>
66 <% end %>
66 <% end %>
67
67
68 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
68 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
69 <h3><%= l(:label_spent_time) %></h3>
69 <h3><%= l(:label_spent_time) %></h3>
70 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
70 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
71 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
71 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
72 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
72 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
73 <% end %>
73 <% end %>
74 <% end %>
74 <% end %>
75
75
76 <% content_for :header_tags do %>
76 <% content_for :header_tags do %>
77 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
77 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
78 <% end %>
78 <% end %>
79
79
80 <% html_title(l(:label_overview)) -%>
80 <% html_title(l(:label_overview)) -%>
@@ -1,31 +1,31
1 <%= error_messages_for 'user' %>
1 <%= error_messages_for 'user' %>
2
2
3 <!--[form:user]-->
3 <!--[form:user]-->
4 <div class="box">
4 <div class="box">
5 <p><%= f.text_field :login, :required => true, :size => 25 %></p>
5 <p><%= f.text_field :login, :required => true, :size => 25 %></p>
6 <p><%= f.text_field :firstname, :required => true %></p>
6 <p><%= f.text_field :firstname, :required => true %></p>
7 <p><%= f.text_field :lastname, :required => true %></p>
7 <p><%= f.text_field :lastname, :required => true %></p>
8 <p><%= f.text_field :mail, :required => true %></p>
8 <p><%= f.text_field :mail, :required => true %></p>
9 <p><%= f.select :language, lang_options_for_select %></p>
9 <p><%= f.select :language, lang_options_for_select %></p>
10
10
11 <% for @custom_value in @custom_values %>
11 <% @user.custom_field_values.each do |value| %>
12 <p><%= custom_field_tag_with_label @custom_value %></p>
12 <p><%= custom_field_tag_with_label :user, value %></p>
13 <% end if @custom_values%>
13 <% end %>
14
14
15 <p><%= f.check_box :admin, :disabled => (@user == User.current) %></p>
15 <p><%= f.check_box :admin, :disabled => (@user == User.current) %></p>
16 </div>
16 </div>
17
17
18 <div class="box">
18 <div class="box">
19 <h3><%=l(:label_authentication)%></h3>
19 <h3><%=l(:label_authentication)%></h3>
20 <% unless @auth_sources.empty? %>
20 <% unless @auth_sources.empty? %>
21 <p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }), {}, :onchange => "if (this.value=='') {Element.show('password_fields');} else {Element.hide('password_fields');}" %></p>
21 <p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }), {}, :onchange => "if (this.value=='') {Element.show('password_fields');} else {Element.hide('password_fields');}" %></p>
22 <% end %>
22 <% end %>
23 <div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
23 <div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
24 <p><label for="password"><%=l(:field_password)%><span class="required"> *</span></label>
24 <p><label for="password"><%=l(:field_password)%><span class="required"> *</span></label>
25 <%= password_field_tag 'password', nil, :size => 25 %><br />
25 <%= password_field_tag 'password', nil, :size => 25 %><br />
26 <em><%= l(:text_caracters_minimum, 4) %></em></p>
26 <em><%= l(:text_caracters_minimum, 4) %></em></p>
27 <p><label for="password_confirmation"><%=l(:field_password_confirmation)%><span class="required"> *</span></label>
27 <p><label for="password_confirmation"><%=l(:field_password_confirmation)%><span class="required"> *</span></label>
28 <%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
28 <%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
29 </div>
29 </div>
30 </div>
30 </div>
31 <!--[eoform:user]-->
31 <!--[eoform:user]-->
@@ -1,65 +1,72
1 ---
1 ---
2 custom_fields_001:
2 custom_fields_001:
3 name: Database
3 name: Database
4 min_length: 0
4 min_length: 0
5 regexp: ""
5 regexp: ""
6 is_for_all: true
6 is_for_all: true
7 is_filter: true
7 is_filter: true
8 type: IssueCustomField
8 type: IssueCustomField
9 max_length: 0
9 max_length: 0
10 possible_values: MySQL|PostgreSQL|Oracle
10 possible_values:
11 - MySQL
12 - PostgreSQL
13 - Oracle
11 id: 1
14 id: 1
12 is_required: false
15 is_required: false
13 field_format: list
16 field_format: list
14 default_value: ""
17 default_value: ""
15 custom_fields_002:
18 custom_fields_002:
16 name: Searchable field
19 name: Searchable field
17 min_length: 1
20 min_length: 1
18 regexp: ""
21 regexp: ""
19 is_for_all: true
22 is_for_all: true
20 type: IssueCustomField
23 type: IssueCustomField
21 max_length: 100
24 max_length: 100
22 possible_values: ""
25 possible_values: ""
23 id: 2
26 id: 2
24 is_required: false
27 is_required: false
25 field_format: string
28 field_format: string
26 searchable: true
29 searchable: true
27 default_value: "Default string"
30 default_value: "Default string"
28 custom_fields_003:
31 custom_fields_003:
29 name: Development status
32 name: Development status
30 min_length: 0
33 min_length: 0
31 regexp: ""
34 regexp: ""
32 is_for_all: false
35 is_for_all: false
33 is_filter: true
36 is_filter: true
34 type: ProjectCustomField
37 type: ProjectCustomField
35 max_length: 0
38 max_length: 0
36 possible_values: Stable|Beta|Alpha|Planning
39 possible_values:
40 - Stable
41 - Beta
42 - Alpha
43 - Planning
37 id: 3
44 id: 3
38 is_required: true
45 is_required: true
39 field_format: list
46 field_format: list
40 default_value: ""
47 default_value: ""
41 custom_fields_004:
48 custom_fields_004:
42 name: Phone number
49 name: Phone number
43 min_length: 0
50 min_length: 0
44 regexp: ""
51 regexp: ""
45 is_for_all: false
52 is_for_all: false
46 type: UserCustomField
53 type: UserCustomField
47 max_length: 0
54 max_length: 0
48 possible_values: ""
55 possible_values: ""
49 id: 4
56 id: 4
50 is_required: false
57 is_required: false
51 field_format: string
58 field_format: string
52 default_value: ""
59 default_value: ""
53 custom_fields_005:
60 custom_fields_005:
54 name: Money
61 name: Money
55 min_length: 0
62 min_length: 0
56 regexp: ""
63 regexp: ""
57 is_for_all: false
64 is_for_all: false
58 type: UserCustomField
65 type: UserCustomField
59 max_length: 0
66 max_length: 0
60 possible_values: ""
67 possible_values: ""
61 id: 5
68 id: 5
62 is_required: false
69 is_required: false
63 field_format: float
70 field_format: float
64 default_value: ""
71 default_value: ""
65 No newline at end of file
72
@@ -1,563 +1,640
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :trackers,
31 :trackers,
32 :projects_trackers,
32 :projects_trackers,
33 :issue_categories,
33 :issue_categories,
34 :enabled_modules,
34 :enabled_modules,
35 :enumerations,
35 :enumerations,
36 :attachments,
36 :attachments,
37 :workflows,
37 :workflows,
38 :custom_fields,
38 :custom_fields,
39 :custom_values,
39 :custom_values,
40 :custom_fields_trackers,
40 :custom_fields_trackers,
41 :time_entries,
41 :time_entries,
42 :journals,
42 :journals,
43 :journal_details
43 :journal_details
44
44
45 def setup
45 def setup
46 @controller = IssuesController.new
46 @controller = IssuesController.new
47 @request = ActionController::TestRequest.new
47 @request = ActionController::TestRequest.new
48 @response = ActionController::TestResponse.new
48 @response = ActionController::TestResponse.new
49 User.current = nil
49 User.current = nil
50 end
50 end
51
51
52 def test_index
52 def test_index
53 get :index
53 get :index
54 assert_response :success
54 assert_response :success
55 assert_template 'index.rhtml'
55 assert_template 'index.rhtml'
56 assert_not_nil assigns(:issues)
56 assert_not_nil assigns(:issues)
57 assert_nil assigns(:project)
57 assert_nil assigns(:project)
58 assert_tag :tag => 'a', :content => /Can't print recipes/
58 assert_tag :tag => 'a', :content => /Can't print recipes/
59 assert_tag :tag => 'a', :content => /Subproject issue/
59 assert_tag :tag => 'a', :content => /Subproject issue/
60 # private projects hidden
60 # private projects hidden
61 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
61 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 assert_no_tag :tag => 'a', :content => /Issue on project 2/
62 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 end
63 end
64
64
65 def test_index_with_project
65 def test_index_with_project
66 Setting.display_subprojects_issues = 0
66 Setting.display_subprojects_issues = 0
67 get :index, :project_id => 1
67 get :index, :project_id => 1
68 assert_response :success
68 assert_response :success
69 assert_template 'index.rhtml'
69 assert_template 'index.rhtml'
70 assert_not_nil assigns(:issues)
70 assert_not_nil assigns(:issues)
71 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 assert_no_tag :tag => 'a', :content => /Subproject issue/
72 assert_no_tag :tag => 'a', :content => /Subproject issue/
73 end
73 end
74
74
75 def test_index_with_project_and_subprojects
75 def test_index_with_project_and_subprojects
76 Setting.display_subprojects_issues = 1
76 Setting.display_subprojects_issues = 1
77 get :index, :project_id => 1
77 get :index, :project_id => 1
78 assert_response :success
78 assert_response :success
79 assert_template 'index.rhtml'
79 assert_template 'index.rhtml'
80 assert_not_nil assigns(:issues)
80 assert_not_nil assigns(:issues)
81 assert_tag :tag => 'a', :content => /Can't print recipes/
81 assert_tag :tag => 'a', :content => /Can't print recipes/
82 assert_tag :tag => 'a', :content => /Subproject issue/
82 assert_tag :tag => 'a', :content => /Subproject issue/
83 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
83 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
84 end
84 end
85
85
86 def test_index_with_project_and_subprojects_should_show_private_subprojects
86 def test_index_with_project_and_subprojects_should_show_private_subprojects
87 @request.session[:user_id] = 2
87 @request.session[:user_id] = 2
88 Setting.display_subprojects_issues = 1
88 Setting.display_subprojects_issues = 1
89 get :index, :project_id => 1
89 get :index, :project_id => 1
90 assert_response :success
90 assert_response :success
91 assert_template 'index.rhtml'
91 assert_template 'index.rhtml'
92 assert_not_nil assigns(:issues)
92 assert_not_nil assigns(:issues)
93 assert_tag :tag => 'a', :content => /Can't print recipes/
93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 assert_tag :tag => 'a', :content => /Subproject issue/
94 assert_tag :tag => 'a', :content => /Subproject issue/
95 assert_tag :tag => 'a', :content => /Issue of a private subproject/
95 assert_tag :tag => 'a', :content => /Issue of a private subproject/
96 end
96 end
97
97
98 def test_index_with_project_and_filter
98 def test_index_with_project_and_filter
99 get :index, :project_id => 1, :set_filter => 1
99 get :index, :project_id => 1, :set_filter => 1
100 assert_response :success
100 assert_response :success
101 assert_template 'index.rhtml'
101 assert_template 'index.rhtml'
102 assert_not_nil assigns(:issues)
102 assert_not_nil assigns(:issues)
103 end
103 end
104
104
105 def test_index_csv_with_project
105 def test_index_csv_with_project
106 get :index, :format => 'csv'
106 get :index, :format => 'csv'
107 assert_response :success
107 assert_response :success
108 assert_not_nil assigns(:issues)
108 assert_not_nil assigns(:issues)
109 assert_equal 'text/csv', @response.content_type
109 assert_equal 'text/csv', @response.content_type
110
110
111 get :index, :project_id => 1, :format => 'csv'
111 get :index, :project_id => 1, :format => 'csv'
112 assert_response :success
112 assert_response :success
113 assert_not_nil assigns(:issues)
113 assert_not_nil assigns(:issues)
114 assert_equal 'text/csv', @response.content_type
114 assert_equal 'text/csv', @response.content_type
115 end
115 end
116
116
117 def test_index_pdf
117 def test_index_pdf
118 get :index, :format => 'pdf'
118 get :index, :format => 'pdf'
119 assert_response :success
119 assert_response :success
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_equal 'application/pdf', @response.content_type
121 assert_equal 'application/pdf', @response.content_type
122
122
123 get :index, :project_id => 1, :format => 'pdf'
123 get :index, :project_id => 1, :format => 'pdf'
124 assert_response :success
124 assert_response :success
125 assert_not_nil assigns(:issues)
125 assert_not_nil assigns(:issues)
126 assert_equal 'application/pdf', @response.content_type
126 assert_equal 'application/pdf', @response.content_type
127 end
127 end
128
128
129 def test_changes
129 def test_changes
130 get :changes, :project_id => 1
130 get :changes, :project_id => 1
131 assert_response :success
131 assert_response :success
132 assert_not_nil assigns(:journals)
132 assert_not_nil assigns(:journals)
133 assert_equal 'application/atom+xml', @response.content_type
133 assert_equal 'application/atom+xml', @response.content_type
134 end
134 end
135
135
136 def test_show_by_anonymous
136 def test_show_by_anonymous
137 get :show, :id => 1
137 get :show, :id => 1
138 assert_response :success
138 assert_response :success
139 assert_template 'show.rhtml'
139 assert_template 'show.rhtml'
140 assert_not_nil assigns(:issue)
140 assert_not_nil assigns(:issue)
141 assert_equal Issue.find(1), assigns(:issue)
141 assert_equal Issue.find(1), assigns(:issue)
142
142
143 # anonymous role is allowed to add a note
143 # anonymous role is allowed to add a note
144 assert_tag :tag => 'form',
144 assert_tag :tag => 'form',
145 :descendant => { :tag => 'fieldset',
145 :descendant => { :tag => 'fieldset',
146 :child => { :tag => 'legend',
146 :child => { :tag => 'legend',
147 :content => /Notes/ } }
147 :content => /Notes/ } }
148 end
148 end
149
149
150 def test_show_by_manager
150 def test_show_by_manager
151 @request.session[:user_id] = 2
151 @request.session[:user_id] = 2
152 get :show, :id => 1
152 get :show, :id => 1
153 assert_response :success
153 assert_response :success
154
154
155 assert_tag :tag => 'form',
155 assert_tag :tag => 'form',
156 :descendant => { :tag => 'fieldset',
156 :descendant => { :tag => 'fieldset',
157 :child => { :tag => 'legend',
157 :child => { :tag => 'legend',
158 :content => /Change properties/ } },
158 :content => /Change properties/ } },
159 :descendant => { :tag => 'fieldset',
159 :descendant => { :tag => 'fieldset',
160 :child => { :tag => 'legend',
160 :child => { :tag => 'legend',
161 :content => /Log time/ } },
161 :content => /Log time/ } },
162 :descendant => { :tag => 'fieldset',
162 :descendant => { :tag => 'fieldset',
163 :child => { :tag => 'legend',
163 :child => { :tag => 'legend',
164 :content => /Notes/ } }
164 :content => /Notes/ } }
165 end
165 end
166
166
167 def test_get_new
167 def test_get_new
168 @request.session[:user_id] = 2
168 @request.session[:user_id] = 2
169 get :new, :project_id => 1, :tracker_id => 1
169 get :new, :project_id => 1, :tracker_id => 1
170 assert_response :success
170 assert_response :success
171 assert_template 'new'
171 assert_template 'new'
172
172
173 assert_tag :tag => 'input', :attributes => { :name => 'custom_fields[2]',
173 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
174 :value => 'Default string' }
174 :value => 'Default string' }
175 end
175 end
176
176
177 def test_get_new_without_tracker_id
177 def test_get_new_without_tracker_id
178 @request.session[:user_id] = 2
178 @request.session[:user_id] = 2
179 get :new, :project_id => 1
179 get :new, :project_id => 1
180 assert_response :success
180 assert_response :success
181 assert_template 'new'
181 assert_template 'new'
182
182
183 issue = assigns(:issue)
183 issue = assigns(:issue)
184 assert_not_nil issue
184 assert_not_nil issue
185 assert_equal Project.find(1).trackers.first, issue.tracker
185 assert_equal Project.find(1).trackers.first, issue.tracker
186 end
186 end
187
187
188 def test_update_new_form
188 def test_update_new_form
189 @request.session[:user_id] = 2
189 @request.session[:user_id] = 2
190 xhr :post, :new, :project_id => 1,
190 xhr :post, :new, :project_id => 1,
191 :issue => {:tracker_id => 2,
191 :issue => {:tracker_id => 2,
192 :subject => 'This is the test_new issue',
192 :subject => 'This is the test_new issue',
193 :description => 'This is the description',
193 :description => 'This is the description',
194 :priority_id => 5}
194 :priority_id => 5}
195 assert_response :success
195 assert_response :success
196 assert_template 'new'
196 assert_template 'new'
197 end
197 end
198
198
199 def test_post_new
199 def test_post_new
200 @request.session[:user_id] = 2
200 @request.session[:user_id] = 2
201 post :new, :project_id => 1,
201 post :new, :project_id => 1,
202 :issue => {:tracker_id => 1,
202 :issue => {:tracker_id => 1,
203 :subject => 'This is the test_new issue',
203 :subject => 'This is the test_new issue',
204 :description => 'This is the description',
204 :description => 'This is the description',
205 :priority_id => 5,
205 :priority_id => 5,
206 :estimated_hours => ''},
206 :estimated_hours => '',
207 :custom_fields => {'2' => 'Value for field 2'}
207 :custom_field_values => {'2' => 'Value for field 2'}}
208 assert_redirected_to 'issues/show'
208 assert_redirected_to 'issues/show'
209
209
210 issue = Issue.find_by_subject('This is the test_new issue')
210 issue = Issue.find_by_subject('This is the test_new issue')
211 assert_not_nil issue
211 assert_not_nil issue
212 assert_equal 2, issue.author_id
212 assert_equal 2, issue.author_id
213 assert_nil issue.estimated_hours
213 assert_nil issue.estimated_hours
214 v = issue.custom_values.find_by_custom_field_id(2)
214 v = issue.custom_values.find_by_custom_field_id(2)
215 assert_not_nil v
215 assert_not_nil v
216 assert_equal 'Value for field 2', v.value
216 assert_equal 'Value for field 2', v.value
217 end
217 end
218
218
219 def test_post_new_without_custom_fields_param
219 def test_post_new_without_custom_fields_param
220 @request.session[:user_id] = 2
220 @request.session[:user_id] = 2
221 post :new, :project_id => 1,
221 post :new, :project_id => 1,
222 :issue => {:tracker_id => 1,
222 :issue => {:tracker_id => 1,
223 :subject => 'This is the test_new issue',
223 :subject => 'This is the test_new issue',
224 :description => 'This is the description',
224 :description => 'This is the description',
225 :priority_id => 5}
225 :priority_id => 5}
226 assert_redirected_to 'issues/show'
226 assert_redirected_to 'issues/show'
227 end
227 end
228
228
229 def test_post_new_with_required_custom_field_and_without_custom_fields_param
230 field = IssueCustomField.find_by_name('Database')
231 field.update_attribute(:is_required, true)
232
233 @request.session[:user_id] = 2
234 post :new, :project_id => 1,
235 :issue => {:tracker_id => 1,
236 :subject => 'This is the test_new issue',
237 :description => 'This is the description',
238 :priority_id => 5}
239 assert_response :success
240 assert_template 'new'
241 issue = assigns(:issue)
242 assert_not_nil issue
243 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
244 end
245
246 def test_post_should_preserve_fields_values_on_validation_failure
247 @request.session[:user_id] = 2
248 post :new, :project_id => 1,
249 :issue => {:tracker_id => 1,
250 :subject => 'This is the test_new issue',
251 # empty description
252 :description => '',
253 :priority_id => 6,
254 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
255 assert_response :success
256 assert_template 'new'
257
258 assert_tag :input, :attributes => { :name => 'issue[subject]',
259 :value => 'This is the test_new issue' }
260 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
261 :child => { :tag => 'option', :attributes => { :selected => 'selected',
262 :value => '6' },
263 :content => 'High' }
264 # Custom fields
265 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
266 :child => { :tag => 'option', :attributes => { :selected => 'selected',
267 :value => 'Oracle' },
268 :content => 'Oracle' }
269 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
270 :value => 'Value for field 2'}
271 end
272
229 def test_copy_issue
273 def test_copy_issue
230 @request.session[:user_id] = 2
274 @request.session[:user_id] = 2
231 get :new, :project_id => 1, :copy_from => 1
275 get :new, :project_id => 1, :copy_from => 1
232 assert_template 'new'
276 assert_template 'new'
233 assert_not_nil assigns(:issue)
277 assert_not_nil assigns(:issue)
234 orig = Issue.find(1)
278 orig = Issue.find(1)
235 assert_equal orig.subject, assigns(:issue).subject
279 assert_equal orig.subject, assigns(:issue).subject
236 end
280 end
237
281
238 def test_get_edit
282 def test_get_edit
239 @request.session[:user_id] = 2
283 @request.session[:user_id] = 2
240 get :edit, :id => 1
284 get :edit, :id => 1
241 assert_response :success
285 assert_response :success
242 assert_template 'edit'
286 assert_template 'edit'
243 assert_not_nil assigns(:issue)
287 assert_not_nil assigns(:issue)
244 assert_equal Issue.find(1), assigns(:issue)
288 assert_equal Issue.find(1), assigns(:issue)
245 end
289 end
246
290
247 def test_get_edit_with_params
291 def test_get_edit_with_params
248 @request.session[:user_id] = 2
292 @request.session[:user_id] = 2
249 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
293 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
250 assert_response :success
294 assert_response :success
251 assert_template 'edit'
295 assert_template 'edit'
252
296
253 issue = assigns(:issue)
297 issue = assigns(:issue)
254 assert_not_nil issue
298 assert_not_nil issue
255
299
256 assert_equal 5, issue.status_id
300 assert_equal 5, issue.status_id
257 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
301 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
258 :child => { :tag => 'option',
302 :child => { :tag => 'option',
259 :content => 'Closed',
303 :content => 'Closed',
260 :attributes => { :selected => 'selected' } }
304 :attributes => { :selected => 'selected' } }
261
305
262 assert_equal 7, issue.priority_id
306 assert_equal 7, issue.priority_id
263 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
307 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
264 :child => { :tag => 'option',
308 :child => { :tag => 'option',
265 :content => 'Urgent',
309 :content => 'Urgent',
266 :attributes => { :selected => 'selected' } }
310 :attributes => { :selected => 'selected' } }
267 end
311 end
268
312
269 def test_reply_to_issue
313 def test_reply_to_issue
270 @request.session[:user_id] = 2
314 @request.session[:user_id] = 2
271 get :reply, :id => 1
315 get :reply, :id => 1
272 assert_response :success
316 assert_response :success
273 assert_select_rjs :show, "update"
317 assert_select_rjs :show, "update"
274 end
318 end
275
319
276 def test_reply_to_note
320 def test_reply_to_note
277 @request.session[:user_id] = 2
321 @request.session[:user_id] = 2
278 get :reply, :id => 1, :journal_id => 2
322 get :reply, :id => 1, :journal_id => 2
279 assert_response :success
323 assert_response :success
280 assert_select_rjs :show, "update"
324 assert_select_rjs :show, "update"
281 end
325 end
282
326
283 def test_post_edit
327 def test_post_edit_without_custom_fields_param
284 @request.session[:user_id] = 2
328 @request.session[:user_id] = 2
285 ActionMailer::Base.deliveries.clear
329 ActionMailer::Base.deliveries.clear
286
330
287 issue = Issue.find(1)
331 issue = Issue.find(1)
332 assert_equal '125', issue.custom_value_for(2).value
288 old_subject = issue.subject
333 old_subject = issue.subject
289 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
334 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
290
335
291 post :edit, :id => 1, :issue => {:subject => new_subject}
336 assert_difference('Journal.count') do
337 assert_difference('JournalDetail.count', 2) do
338 post :edit, :id => 1, :issue => {:subject => new_subject,
339 :priority_id => '6',
340 :category_id => '1' # no change
341 }
342 end
343 end
292 assert_redirected_to 'issues/show/1'
344 assert_redirected_to 'issues/show/1'
293 issue.reload
345 issue.reload
294 assert_equal new_subject, issue.subject
346 assert_equal new_subject, issue.subject
347 # Make sure custom fields were not cleared
348 assert_equal '125', issue.custom_value_for(2).value
295
349
296 mail = ActionMailer::Base.deliveries.last
350 mail = ActionMailer::Base.deliveries.last
297 assert_kind_of TMail::Mail, mail
351 assert_kind_of TMail::Mail, mail
298 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
352 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
299 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
353 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
300 end
354 end
301
355
356 def test_post_edit_with_custom_field_change
357 @request.session[:user_id] = 2
358 issue = Issue.find(1)
359 assert_equal '125', issue.custom_value_for(2).value
360
361 assert_difference('Journal.count') do
362 assert_difference('JournalDetail.count', 3) do
363 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
364 :priority_id => '6',
365 :category_id => '1', # no change
366 :custom_field_values => { '2' => 'New custom value' }
367 }
368 end
369 end
370 assert_redirected_to 'issues/show/1'
371 issue.reload
372 assert_equal 'New custom value', issue.custom_value_for(2).value
373
374 mail = ActionMailer::Base.deliveries.last
375 assert_kind_of TMail::Mail, mail
376 assert mail.body.include?("Searchable field changed from 125 to New custom value")
377 end
378
302 def test_post_edit_with_status_and_assignee_change
379 def test_post_edit_with_status_and_assignee_change
303 issue = Issue.find(1)
380 issue = Issue.find(1)
304 assert_equal 1, issue.status_id
381 assert_equal 1, issue.status_id
305 @request.session[:user_id] = 2
382 @request.session[:user_id] = 2
306 assert_difference('TimeEntry.count', 0) do
383 assert_difference('TimeEntry.count', 0) do
307 post :edit,
384 post :edit,
308 :id => 1,
385 :id => 1,
309 :issue => { :status_id => 2, :assigned_to_id => 3 },
386 :issue => { :status_id => 2, :assigned_to_id => 3 },
310 :notes => 'Assigned to dlopper',
387 :notes => 'Assigned to dlopper',
311 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
388 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
312 end
389 end
313 assert_redirected_to 'issues/show/1'
390 assert_redirected_to 'issues/show/1'
314 issue.reload
391 issue.reload
315 assert_equal 2, issue.status_id
392 assert_equal 2, issue.status_id
316 j = issue.journals.find(:first, :order => 'id DESC')
393 j = issue.journals.find(:first, :order => 'id DESC')
317 assert_equal 'Assigned to dlopper', j.notes
394 assert_equal 'Assigned to dlopper', j.notes
318 assert_equal 2, j.details.size
395 assert_equal 2, j.details.size
319
396
320 mail = ActionMailer::Base.deliveries.last
397 mail = ActionMailer::Base.deliveries.last
321 assert mail.body.include?("Status changed from New to Assigned")
398 assert mail.body.include?("Status changed from New to Assigned")
322 end
399 end
323
400
324 def test_post_edit_with_note_only
401 def test_post_edit_with_note_only
325 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
402 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
326 # anonymous user
403 # anonymous user
327 post :edit,
404 post :edit,
328 :id => 1,
405 :id => 1,
329 :notes => notes
406 :notes => notes
330 assert_redirected_to 'issues/show/1'
407 assert_redirected_to 'issues/show/1'
331 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
408 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
332 assert_equal notes, j.notes
409 assert_equal notes, j.notes
333 assert_equal 0, j.details.size
410 assert_equal 0, j.details.size
334 assert_equal User.anonymous, j.user
411 assert_equal User.anonymous, j.user
335
412
336 mail = ActionMailer::Base.deliveries.last
413 mail = ActionMailer::Base.deliveries.last
337 assert mail.body.include?(notes)
414 assert mail.body.include?(notes)
338 end
415 end
339
416
340 def test_post_edit_with_note_and_spent_time
417 def test_post_edit_with_note_and_spent_time
341 @request.session[:user_id] = 2
418 @request.session[:user_id] = 2
342 spent_hours_before = Issue.find(1).spent_hours
419 spent_hours_before = Issue.find(1).spent_hours
343 assert_difference('TimeEntry.count') do
420 assert_difference('TimeEntry.count') do
344 post :edit,
421 post :edit,
345 :id => 1,
422 :id => 1,
346 :notes => '2.5 hours added',
423 :notes => '2.5 hours added',
347 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
424 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
348 end
425 end
349 assert_redirected_to 'issues/show/1'
426 assert_redirected_to 'issues/show/1'
350
427
351 issue = Issue.find(1)
428 issue = Issue.find(1)
352
429
353 j = issue.journals.find(:first, :order => 'id DESC')
430 j = issue.journals.find(:first, :order => 'id DESC')
354 assert_equal '2.5 hours added', j.notes
431 assert_equal '2.5 hours added', j.notes
355 assert_equal 0, j.details.size
432 assert_equal 0, j.details.size
356
433
357 t = issue.time_entries.find(:first, :order => 'id DESC')
434 t = issue.time_entries.find(:first, :order => 'id DESC')
358 assert_not_nil t
435 assert_not_nil t
359 assert_equal 2.5, t.hours
436 assert_equal 2.5, t.hours
360 assert_equal spent_hours_before + 2.5, issue.spent_hours
437 assert_equal spent_hours_before + 2.5, issue.spent_hours
361 end
438 end
362
439
363 def test_post_edit_with_attachment_only
440 def test_post_edit_with_attachment_only
364 set_tmp_attachments_directory
441 set_tmp_attachments_directory
365
442
366 # anonymous user
443 # anonymous user
367 post :edit,
444 post :edit,
368 :id => 1,
445 :id => 1,
369 :notes => '',
446 :notes => '',
370 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
447 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
371 assert_redirected_to 'issues/show/1'
448 assert_redirected_to 'issues/show/1'
372 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
449 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
373 assert j.notes.blank?
450 assert j.notes.blank?
374 assert_equal 1, j.details.size
451 assert_equal 1, j.details.size
375 assert_equal 'testfile.txt', j.details.first.value
452 assert_equal 'testfile.txt', j.details.first.value
376 assert_equal User.anonymous, j.user
453 assert_equal User.anonymous, j.user
377
454
378 mail = ActionMailer::Base.deliveries.last
455 mail = ActionMailer::Base.deliveries.last
379 assert mail.body.include?('testfile.txt')
456 assert mail.body.include?('testfile.txt')
380 end
457 end
381
458
382 def test_post_edit_with_no_change
459 def test_post_edit_with_no_change
383 issue = Issue.find(1)
460 issue = Issue.find(1)
384 issue.journals.clear
461 issue.journals.clear
385 ActionMailer::Base.deliveries.clear
462 ActionMailer::Base.deliveries.clear
386
463
387 post :edit,
464 post :edit,
388 :id => 1,
465 :id => 1,
389 :notes => ''
466 :notes => ''
390 assert_redirected_to 'issues/show/1'
467 assert_redirected_to 'issues/show/1'
391
468
392 issue.reload
469 issue.reload
393 assert issue.journals.empty?
470 assert issue.journals.empty?
394 # No email should be sent
471 # No email should be sent
395 assert ActionMailer::Base.deliveries.empty?
472 assert ActionMailer::Base.deliveries.empty?
396 end
473 end
397
474
398 def test_bulk_edit
475 def test_bulk_edit
399 @request.session[:user_id] = 2
476 @request.session[:user_id] = 2
400 # update issues priority
477 # update issues priority
401 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
478 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
402 assert_response 302
479 assert_response 302
403 # check that the issues were updated
480 # check that the issues were updated
404 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
481 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
405 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
482 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
406 end
483 end
407
484
408 def test_bulk_unassign
485 def test_bulk_unassign
409 assert_not_nil Issue.find(2).assigned_to
486 assert_not_nil Issue.find(2).assigned_to
410 @request.session[:user_id] = 2
487 @request.session[:user_id] = 2
411 # unassign issues
488 # unassign issues
412 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
489 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
413 assert_response 302
490 assert_response 302
414 # check that the issues were updated
491 # check that the issues were updated
415 assert_nil Issue.find(2).assigned_to
492 assert_nil Issue.find(2).assigned_to
416 end
493 end
417
494
418 def test_move_one_issue_to_another_project
495 def test_move_one_issue_to_another_project
419 @request.session[:user_id] = 1
496 @request.session[:user_id] = 1
420 post :move, :id => 1, :new_project_id => 2
497 post :move, :id => 1, :new_project_id => 2
421 assert_redirected_to 'projects/ecookbook/issues'
498 assert_redirected_to 'projects/ecookbook/issues'
422 assert_equal 2, Issue.find(1).project_id
499 assert_equal 2, Issue.find(1).project_id
423 end
500 end
424
501
425 def test_bulk_move_to_another_project
502 def test_bulk_move_to_another_project
426 @request.session[:user_id] = 1
503 @request.session[:user_id] = 1
427 post :move, :ids => [1, 2], :new_project_id => 2
504 post :move, :ids => [1, 2], :new_project_id => 2
428 assert_redirected_to 'projects/ecookbook/issues'
505 assert_redirected_to 'projects/ecookbook/issues'
429 # Issues moved to project 2
506 # Issues moved to project 2
430 assert_equal 2, Issue.find(1).project_id
507 assert_equal 2, Issue.find(1).project_id
431 assert_equal 2, Issue.find(2).project_id
508 assert_equal 2, Issue.find(2).project_id
432 # No tracker change
509 # No tracker change
433 assert_equal 1, Issue.find(1).tracker_id
510 assert_equal 1, Issue.find(1).tracker_id
434 assert_equal 2, Issue.find(2).tracker_id
511 assert_equal 2, Issue.find(2).tracker_id
435 end
512 end
436
513
437 def test_bulk_move_to_another_tracker
514 def test_bulk_move_to_another_tracker
438 @request.session[:user_id] = 1
515 @request.session[:user_id] = 1
439 post :move, :ids => [1, 2], :new_tracker_id => 2
516 post :move, :ids => [1, 2], :new_tracker_id => 2
440 assert_redirected_to 'projects/ecookbook/issues'
517 assert_redirected_to 'projects/ecookbook/issues'
441 assert_equal 2, Issue.find(1).tracker_id
518 assert_equal 2, Issue.find(1).tracker_id
442 assert_equal 2, Issue.find(2).tracker_id
519 assert_equal 2, Issue.find(2).tracker_id
443 end
520 end
444
521
445 def test_context_menu_one_issue
522 def test_context_menu_one_issue
446 @request.session[:user_id] = 2
523 @request.session[:user_id] = 2
447 get :context_menu, :ids => [1]
524 get :context_menu, :ids => [1]
448 assert_response :success
525 assert_response :success
449 assert_template 'context_menu'
526 assert_template 'context_menu'
450 assert_tag :tag => 'a', :content => 'Edit',
527 assert_tag :tag => 'a', :content => 'Edit',
451 :attributes => { :href => '/issues/edit/1',
528 :attributes => { :href => '/issues/edit/1',
452 :class => 'icon-edit' }
529 :class => 'icon-edit' }
453 assert_tag :tag => 'a', :content => 'Closed',
530 assert_tag :tag => 'a', :content => 'Closed',
454 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
531 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
455 :class => '' }
532 :class => '' }
456 assert_tag :tag => 'a', :content => 'Immediate',
533 assert_tag :tag => 'a', :content => 'Immediate',
457 :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
534 :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
458 :class => '' }
535 :class => '' }
459 assert_tag :tag => 'a', :content => 'Dave Lopper',
536 assert_tag :tag => 'a', :content => 'Dave Lopper',
460 :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
537 :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
461 :class => '' }
538 :class => '' }
462 assert_tag :tag => 'a', :content => 'Copy',
539 assert_tag :tag => 'a', :content => 'Copy',
463 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
540 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
464 :class => 'icon-copy' }
541 :class => 'icon-copy' }
465 assert_tag :tag => 'a', :content => 'Move',
542 assert_tag :tag => 'a', :content => 'Move',
466 :attributes => { :href => '/issues/move?ids%5B%5D=1',
543 :attributes => { :href => '/issues/move?ids%5B%5D=1',
467 :class => 'icon-move' }
544 :class => 'icon-move' }
468 assert_tag :tag => 'a', :content => 'Delete',
545 assert_tag :tag => 'a', :content => 'Delete',
469 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
546 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
470 :class => 'icon-del' }
547 :class => 'icon-del' }
471 end
548 end
472
549
473 def test_context_menu_one_issue_by_anonymous
550 def test_context_menu_one_issue_by_anonymous
474 get :context_menu, :ids => [1]
551 get :context_menu, :ids => [1]
475 assert_response :success
552 assert_response :success
476 assert_template 'context_menu'
553 assert_template 'context_menu'
477 assert_tag :tag => 'a', :content => 'Delete',
554 assert_tag :tag => 'a', :content => 'Delete',
478 :attributes => { :href => '#',
555 :attributes => { :href => '#',
479 :class => 'icon-del disabled' }
556 :class => 'icon-del disabled' }
480 end
557 end
481
558
482 def test_context_menu_multiple_issues_of_same_project
559 def test_context_menu_multiple_issues_of_same_project
483 @request.session[:user_id] = 2
560 @request.session[:user_id] = 2
484 get :context_menu, :ids => [1, 2]
561 get :context_menu, :ids => [1, 2]
485 assert_response :success
562 assert_response :success
486 assert_template 'context_menu'
563 assert_template 'context_menu'
487 assert_tag :tag => 'a', :content => 'Edit',
564 assert_tag :tag => 'a', :content => 'Edit',
488 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
565 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
489 :class => 'icon-edit' }
566 :class => 'icon-edit' }
490 assert_tag :tag => 'a', :content => 'Move',
567 assert_tag :tag => 'a', :content => 'Move',
491 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
568 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
492 :class => 'icon-move' }
569 :class => 'icon-move' }
493 assert_tag :tag => 'a', :content => 'Delete',
570 assert_tag :tag => 'a', :content => 'Delete',
494 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
571 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
495 :class => 'icon-del' }
572 :class => 'icon-del' }
496 end
573 end
497
574
498 def test_context_menu_multiple_issues_of_different_project
575 def test_context_menu_multiple_issues_of_different_project
499 @request.session[:user_id] = 2
576 @request.session[:user_id] = 2
500 get :context_menu, :ids => [1, 2, 4]
577 get :context_menu, :ids => [1, 2, 4]
501 assert_response :success
578 assert_response :success
502 assert_template 'context_menu'
579 assert_template 'context_menu'
503 assert_tag :tag => 'a', :content => 'Delete',
580 assert_tag :tag => 'a', :content => 'Delete',
504 :attributes => { :href => '#',
581 :attributes => { :href => '#',
505 :class => 'icon-del disabled' }
582 :class => 'icon-del disabled' }
506 end
583 end
507
584
508 def test_destroy_issue_with_no_time_entries
585 def test_destroy_issue_with_no_time_entries
509 assert_nil TimeEntry.find_by_issue_id(2)
586 assert_nil TimeEntry.find_by_issue_id(2)
510 @request.session[:user_id] = 2
587 @request.session[:user_id] = 2
511 post :destroy, :id => 2
588 post :destroy, :id => 2
512 assert_redirected_to 'projects/ecookbook/issues'
589 assert_redirected_to 'projects/ecookbook/issues'
513 assert_nil Issue.find_by_id(2)
590 assert_nil Issue.find_by_id(2)
514 end
591 end
515
592
516 def test_destroy_issues_with_time_entries
593 def test_destroy_issues_with_time_entries
517 @request.session[:user_id] = 2
594 @request.session[:user_id] = 2
518 post :destroy, :ids => [1, 3]
595 post :destroy, :ids => [1, 3]
519 assert_response :success
596 assert_response :success
520 assert_template 'destroy'
597 assert_template 'destroy'
521 assert_not_nil assigns(:hours)
598 assert_not_nil assigns(:hours)
522 assert Issue.find_by_id(1) && Issue.find_by_id(3)
599 assert Issue.find_by_id(1) && Issue.find_by_id(3)
523 end
600 end
524
601
525 def test_destroy_issues_and_destroy_time_entries
602 def test_destroy_issues_and_destroy_time_entries
526 @request.session[:user_id] = 2
603 @request.session[:user_id] = 2
527 post :destroy, :ids => [1, 3], :todo => 'destroy'
604 post :destroy, :ids => [1, 3], :todo => 'destroy'
528 assert_redirected_to 'projects/ecookbook/issues'
605 assert_redirected_to 'projects/ecookbook/issues'
529 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
606 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
530 assert_nil TimeEntry.find_by_id([1, 2])
607 assert_nil TimeEntry.find_by_id([1, 2])
531 end
608 end
532
609
533 def test_destroy_issues_and_assign_time_entries_to_project
610 def test_destroy_issues_and_assign_time_entries_to_project
534 @request.session[:user_id] = 2
611 @request.session[:user_id] = 2
535 post :destroy, :ids => [1, 3], :todo => 'nullify'
612 post :destroy, :ids => [1, 3], :todo => 'nullify'
536 assert_redirected_to 'projects/ecookbook/issues'
613 assert_redirected_to 'projects/ecookbook/issues'
537 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
614 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
538 assert_nil TimeEntry.find(1).issue_id
615 assert_nil TimeEntry.find(1).issue_id
539 assert_nil TimeEntry.find(2).issue_id
616 assert_nil TimeEntry.find(2).issue_id
540 end
617 end
541
618
542 def test_destroy_issues_and_reassign_time_entries_to_another_issue
619 def test_destroy_issues_and_reassign_time_entries_to_another_issue
543 @request.session[:user_id] = 2
620 @request.session[:user_id] = 2
544 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
621 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
545 assert_redirected_to 'projects/ecookbook/issues'
622 assert_redirected_to 'projects/ecookbook/issues'
546 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
623 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
547 assert_equal 2, TimeEntry.find(1).issue_id
624 assert_equal 2, TimeEntry.find(1).issue_id
548 assert_equal 2, TimeEntry.find(2).issue_id
625 assert_equal 2, TimeEntry.find(2).issue_id
549 end
626 end
550
627
551 def test_destroy_attachment
628 def test_destroy_attachment
552 issue = Issue.find(3)
629 issue = Issue.find(3)
553 a = issue.attachments.size
630 a = issue.attachments.size
554 @request.session[:user_id] = 2
631 @request.session[:user_id] = 2
555 post :destroy_attachment, :id => 3, :attachment_id => 1
632 post :destroy_attachment, :id => 3, :attachment_id => 1
556 assert_redirected_to 'issues/show/3'
633 assert_redirected_to 'issues/show/3'
557 assert_nil Attachment.find_by_id(1)
634 assert_nil Attachment.find_by_id(1)
558 issue.reload
635 issue.reload
559 assert_equal((a-1), issue.attachments.size)
636 assert_equal((a-1), issue.attachments.size)
560 j = issue.journals.find(:first, :order => 'created_on DESC')
637 j = issue.journals.find(:first, :order => 'created_on DESC')
561 assert_equal 'attachment', j.details.first.property
638 assert_equal 'attachment', j.details.first.property
562 end
639 end
563 end
640 end
@@ -1,307 +1,307
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
27
27
28 def setup
28 def setup
29 @controller = ProjectsController.new
29 @controller = ProjectsController.new
30 @request = ActionController::TestRequest.new
30 @request = ActionController::TestRequest.new
31 @response = ActionController::TestResponse.new
31 @response = ActionController::TestResponse.new
32 @request.session[:user_id] = nil
32 @request.session[:user_id] = nil
33 end
33 end
34
34
35 def test_index
35 def test_index
36 get :index
36 get :index
37 assert_response :success
37 assert_response :success
38 assert_template 'index'
38 assert_template 'index'
39 assert_not_nil assigns(:project_tree)
39 assert_not_nil assigns(:project_tree)
40 # Root project as hash key
40 # Root project as hash key
41 assert assigns(:project_tree).has_key?(Project.find(1))
41 assert assigns(:project_tree).has_key?(Project.find(1))
42 # Subproject in corresponding value
42 # Subproject in corresponding value
43 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
43 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
44 end
44 end
45
45
46 def test_show_by_id
46 def test_show_by_id
47 get :show, :id => 1
47 get :show, :id => 1
48 assert_response :success
48 assert_response :success
49 assert_template 'show'
49 assert_template 'show'
50 assert_not_nil assigns(:project)
50 assert_not_nil assigns(:project)
51 end
51 end
52
52
53 def test_show_by_identifier
53 def test_show_by_identifier
54 get :show, :id => 'ecookbook'
54 get :show, :id => 'ecookbook'
55 assert_response :success
55 assert_response :success
56 assert_template 'show'
56 assert_template 'show'
57 assert_not_nil assigns(:project)
57 assert_not_nil assigns(:project)
58 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
58 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
59 end
59 end
60
60
61 def test_private_subprojects_hidden
61 def test_private_subprojects_hidden
62 get :show, :id => 'ecookbook'
62 get :show, :id => 'ecookbook'
63 assert_response :success
63 assert_response :success
64 assert_template 'show'
64 assert_template 'show'
65 assert_no_tag :tag => 'a', :content => /Private child/
65 assert_no_tag :tag => 'a', :content => /Private child/
66 end
66 end
67
67
68 def test_private_subprojects_visible
68 def test_private_subprojects_visible
69 @request.session[:user_id] = 2 # manager who is a member of the private subproject
69 @request.session[:user_id] = 2 # manager who is a member of the private subproject
70 get :show, :id => 'ecookbook'
70 get :show, :id => 'ecookbook'
71 assert_response :success
71 assert_response :success
72 assert_template 'show'
72 assert_template 'show'
73 assert_tag :tag => 'a', :content => /Private child/
73 assert_tag :tag => 'a', :content => /Private child/
74 end
74 end
75
75
76 def test_settings
76 def test_settings
77 @request.session[:user_id] = 2 # manager
77 @request.session[:user_id] = 2 # manager
78 get :settings, :id => 1
78 get :settings, :id => 1
79 assert_response :success
79 assert_response :success
80 assert_template 'settings'
80 assert_template 'settings'
81 end
81 end
82
82
83 def test_edit
83 def test_edit
84 @request.session[:user_id] = 2 # manager
84 @request.session[:user_id] = 2 # manager
85 post :edit, :id => 1, :project => {:name => 'Test changed name',
85 post :edit, :id => 1, :project => {:name => 'Test changed name',
86 :custom_field_ids => ['']}
86 :issue_custom_field_ids => ['']}
87 assert_redirected_to 'projects/settings/ecookbook'
87 assert_redirected_to 'projects/settings/ecookbook'
88 project = Project.find(1)
88 project = Project.find(1)
89 assert_equal 'Test changed name', project.name
89 assert_equal 'Test changed name', project.name
90 end
90 end
91
91
92 def test_get_destroy
92 def test_get_destroy
93 @request.session[:user_id] = 1 # admin
93 @request.session[:user_id] = 1 # admin
94 get :destroy, :id => 1
94 get :destroy, :id => 1
95 assert_response :success
95 assert_response :success
96 assert_template 'destroy'
96 assert_template 'destroy'
97 assert_not_nil Project.find_by_id(1)
97 assert_not_nil Project.find_by_id(1)
98 end
98 end
99
99
100 def test_post_destroy
100 def test_post_destroy
101 @request.session[:user_id] = 1 # admin
101 @request.session[:user_id] = 1 # admin
102 post :destroy, :id => 1, :confirm => 1
102 post :destroy, :id => 1, :confirm => 1
103 assert_redirected_to 'admin/projects'
103 assert_redirected_to 'admin/projects'
104 assert_nil Project.find_by_id(1)
104 assert_nil Project.find_by_id(1)
105 end
105 end
106
106
107 def test_list_files
107 def test_list_files
108 get :list_files, :id => 1
108 get :list_files, :id => 1
109 assert_response :success
109 assert_response :success
110 assert_template 'list_files'
110 assert_template 'list_files'
111 assert_not_nil assigns(:versions)
111 assert_not_nil assigns(:versions)
112 end
112 end
113
113
114 def test_changelog
114 def test_changelog
115 get :changelog, :id => 1
115 get :changelog, :id => 1
116 assert_response :success
116 assert_response :success
117 assert_template 'changelog'
117 assert_template 'changelog'
118 assert_not_nil assigns(:versions)
118 assert_not_nil assigns(:versions)
119 end
119 end
120
120
121 def test_roadmap
121 def test_roadmap
122 get :roadmap, :id => 1
122 get :roadmap, :id => 1
123 assert_response :success
123 assert_response :success
124 assert_template 'roadmap'
124 assert_template 'roadmap'
125 assert_not_nil assigns(:versions)
125 assert_not_nil assigns(:versions)
126 # Version with no date set appears
126 # Version with no date set appears
127 assert assigns(:versions).include?(Version.find(3))
127 assert assigns(:versions).include?(Version.find(3))
128 # Completed version doesn't appear
128 # Completed version doesn't appear
129 assert !assigns(:versions).include?(Version.find(1))
129 assert !assigns(:versions).include?(Version.find(1))
130 end
130 end
131
131
132 def test_roadmap_with_completed_versions
132 def test_roadmap_with_completed_versions
133 get :roadmap, :id => 1, :completed => 1
133 get :roadmap, :id => 1, :completed => 1
134 assert_response :success
134 assert_response :success
135 assert_template 'roadmap'
135 assert_template 'roadmap'
136 assert_not_nil assigns(:versions)
136 assert_not_nil assigns(:versions)
137 # Version with no date set appears
137 # Version with no date set appears
138 assert assigns(:versions).include?(Version.find(3))
138 assert assigns(:versions).include?(Version.find(3))
139 # Completed version appears
139 # Completed version appears
140 assert assigns(:versions).include?(Version.find(1))
140 assert assigns(:versions).include?(Version.find(1))
141 end
141 end
142
142
143 def test_project_activity
143 def test_project_activity
144 get :activity, :id => 1, :with_subprojects => 0
144 get :activity, :id => 1, :with_subprojects => 0
145 assert_response :success
145 assert_response :success
146 assert_template 'activity'
146 assert_template 'activity'
147 assert_not_nil assigns(:events_by_day)
147 assert_not_nil assigns(:events_by_day)
148 assert_not_nil assigns(:events)
148 assert_not_nil assigns(:events)
149
149
150 # subproject issue not included by default
150 # subproject issue not included by default
151 assert !assigns(:events).include?(Issue.find(5))
151 assert !assigns(:events).include?(Issue.find(5))
152
152
153 assert_tag :tag => "h3",
153 assert_tag :tag => "h3",
154 :content => /#{2.days.ago.to_date.day}/,
154 :content => /#{2.days.ago.to_date.day}/,
155 :sibling => { :tag => "dl",
155 :sibling => { :tag => "dl",
156 :child => { :tag => "dt",
156 :child => { :tag => "dt",
157 :attributes => { :class => /issue-edit/ },
157 :attributes => { :class => /issue-edit/ },
158 :child => { :tag => "a",
158 :child => { :tag => "a",
159 :content => /(#{IssueStatus.find(2).name})/,
159 :content => /(#{IssueStatus.find(2).name})/,
160 }
160 }
161 }
161 }
162 }
162 }
163
163
164 get :activity, :id => 1, :from => 3.days.ago.to_date
164 get :activity, :id => 1, :from => 3.days.ago.to_date
165 assert_response :success
165 assert_response :success
166 assert_template 'activity'
166 assert_template 'activity'
167 assert_not_nil assigns(:events_by_day)
167 assert_not_nil assigns(:events_by_day)
168
168
169 assert_tag :tag => "h3",
169 assert_tag :tag => "h3",
170 :content => /#{3.day.ago.to_date.day}/,
170 :content => /#{3.day.ago.to_date.day}/,
171 :sibling => { :tag => "dl",
171 :sibling => { :tag => "dl",
172 :child => { :tag => "dt",
172 :child => { :tag => "dt",
173 :attributes => { :class => /issue/ },
173 :attributes => { :class => /issue/ },
174 :child => { :tag => "a",
174 :child => { :tag => "a",
175 :content => /#{Issue.find(1).subject}/,
175 :content => /#{Issue.find(1).subject}/,
176 }
176 }
177 }
177 }
178 }
178 }
179 end
179 end
180
180
181 def test_activity_with_subprojects
181 def test_activity_with_subprojects
182 get :activity, :id => 1, :with_subprojects => 1
182 get :activity, :id => 1, :with_subprojects => 1
183 assert_response :success
183 assert_response :success
184 assert_template 'activity'
184 assert_template 'activity'
185 assert_not_nil assigns(:events)
185 assert_not_nil assigns(:events)
186
186
187 assert assigns(:events).include?(Issue.find(1))
187 assert assigns(:events).include?(Issue.find(1))
188 assert !assigns(:events).include?(Issue.find(4))
188 assert !assigns(:events).include?(Issue.find(4))
189 # subproject issue
189 # subproject issue
190 assert assigns(:events).include?(Issue.find(5))
190 assert assigns(:events).include?(Issue.find(5))
191 end
191 end
192
192
193 def test_global_activity_anonymous
193 def test_global_activity_anonymous
194 get :activity
194 get :activity
195 assert_response :success
195 assert_response :success
196 assert_template 'activity'
196 assert_template 'activity'
197 assert_not_nil assigns(:events)
197 assert_not_nil assigns(:events)
198
198
199 assert assigns(:events).include?(Issue.find(1))
199 assert assigns(:events).include?(Issue.find(1))
200 # Issue of a private project
200 # Issue of a private project
201 assert !assigns(:events).include?(Issue.find(4))
201 assert !assigns(:events).include?(Issue.find(4))
202 end
202 end
203
203
204 def test_global_activity_logged_user
204 def test_global_activity_logged_user
205 @request.session[:user_id] = 2 # manager
205 @request.session[:user_id] = 2 # manager
206 get :activity
206 get :activity
207 assert_response :success
207 assert_response :success
208 assert_template 'activity'
208 assert_template 'activity'
209 assert_not_nil assigns(:events)
209 assert_not_nil assigns(:events)
210
210
211 assert assigns(:events).include?(Issue.find(1))
211 assert assigns(:events).include?(Issue.find(1))
212 # Issue of a private project the user belongs to
212 # Issue of a private project the user belongs to
213 assert assigns(:events).include?(Issue.find(4))
213 assert assigns(:events).include?(Issue.find(4))
214 end
214 end
215
215
216
216
217 def test_global_activity_with_all_types
217 def test_global_activity_with_all_types
218 get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1
218 get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1
219 assert_response :success
219 assert_response :success
220 assert_template 'activity'
220 assert_template 'activity'
221 assert_not_nil assigns(:events)
221 assert_not_nil assigns(:events)
222
222
223 assert assigns(:events).include?(Issue.find(1))
223 assert assigns(:events).include?(Issue.find(1))
224 assert !assigns(:events).include?(Issue.find(4))
224 assert !assigns(:events).include?(Issue.find(4))
225 assert assigns(:events).include?(Message.find(5))
225 assert assigns(:events).include?(Message.find(5))
226 end
226 end
227
227
228 def test_calendar
228 def test_calendar
229 get :calendar, :id => 1
229 get :calendar, :id => 1
230 assert_response :success
230 assert_response :success
231 assert_template 'calendar'
231 assert_template 'calendar'
232 assert_not_nil assigns(:calendar)
232 assert_not_nil assigns(:calendar)
233 end
233 end
234
234
235 def test_calendar_with_subprojects_should_not_show_private_subprojects
235 def test_calendar_with_subprojects_should_not_show_private_subprojects
236 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
236 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
237 assert_response :success
237 assert_response :success
238 assert_template 'calendar'
238 assert_template 'calendar'
239 assert_not_nil assigns(:calendar)
239 assert_not_nil assigns(:calendar)
240 assert_no_tag :tag => 'a', :content => /#6/
240 assert_no_tag :tag => 'a', :content => /#6/
241 end
241 end
242
242
243 def test_calendar_with_subprojects_should_show_private_subprojects
243 def test_calendar_with_subprojects_should_show_private_subprojects
244 @request.session[:user_id] = 2
244 @request.session[:user_id] = 2
245 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
245 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
246 assert_response :success
246 assert_response :success
247 assert_template 'calendar'
247 assert_template 'calendar'
248 assert_not_nil assigns(:calendar)
248 assert_not_nil assigns(:calendar)
249 assert_tag :tag => 'a', :content => /#6/
249 assert_tag :tag => 'a', :content => /#6/
250 end
250 end
251
251
252 def test_gantt
252 def test_gantt
253 get :gantt, :id => 1
253 get :gantt, :id => 1
254 assert_response :success
254 assert_response :success
255 assert_template 'gantt.rhtml'
255 assert_template 'gantt.rhtml'
256 events = assigns(:events)
256 events = assigns(:events)
257 assert_not_nil events
257 assert_not_nil events
258 # Issue with start and due dates
258 # Issue with start and due dates
259 i = Issue.find(1)
259 i = Issue.find(1)
260 assert_not_nil i.due_date
260 assert_not_nil i.due_date
261 assert events.include?(Issue.find(1))
261 assert events.include?(Issue.find(1))
262 # Issue with without due date but targeted to a version with date
262 # Issue with without due date but targeted to a version with date
263 i = Issue.find(2)
263 i = Issue.find(2)
264 assert_nil i.due_date
264 assert_nil i.due_date
265 assert events.include?(i)
265 assert events.include?(i)
266 end
266 end
267
267
268 def test_gantt_with_subprojects_should_not_show_private_subprojects
268 def test_gantt_with_subprojects_should_not_show_private_subprojects
269 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
269 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
270 assert_response :success
270 assert_response :success
271 assert_template 'gantt.rhtml'
271 assert_template 'gantt.rhtml'
272 assert_not_nil assigns(:events)
272 assert_not_nil assigns(:events)
273 assert_no_tag :tag => 'a', :content => /#6/
273 assert_no_tag :tag => 'a', :content => /#6/
274 end
274 end
275
275
276 def test_gantt_with_subprojects_should_show_private_subprojects
276 def test_gantt_with_subprojects_should_show_private_subprojects
277 @request.session[:user_id] = 2
277 @request.session[:user_id] = 2
278 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
278 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
279 assert_response :success
279 assert_response :success
280 assert_template 'gantt.rhtml'
280 assert_template 'gantt.rhtml'
281 assert_not_nil assigns(:events)
281 assert_not_nil assigns(:events)
282 assert_tag :tag => 'a', :content => /#6/
282 assert_tag :tag => 'a', :content => /#6/
283 end
283 end
284
284
285 def test_gantt_export_to_pdf
285 def test_gantt_export_to_pdf
286 get :gantt, :id => 1, :format => 'pdf'
286 get :gantt, :id => 1, :format => 'pdf'
287 assert_response :success
287 assert_response :success
288 assert_template 'gantt.rfpdf'
288 assert_template 'gantt.rfpdf'
289 assert_equal 'application/pdf', @response.content_type
289 assert_equal 'application/pdf', @response.content_type
290 assert_not_nil assigns(:events)
290 assert_not_nil assigns(:events)
291 end
291 end
292
292
293 def test_archive
293 def test_archive
294 @request.session[:user_id] = 1 # admin
294 @request.session[:user_id] = 1 # admin
295 post :archive, :id => 1
295 post :archive, :id => 1
296 assert_redirected_to 'admin/projects'
296 assert_redirected_to 'admin/projects'
297 assert !Project.find(1).active?
297 assert !Project.find(1).active?
298 end
298 end
299
299
300 def test_unarchive
300 def test_unarchive
301 @request.session[:user_id] = 1 # admin
301 @request.session[:user_id] = 1 # admin
302 Project.find(1).archive
302 Project.find(1).archive
303 post :unarchive, :id => 1
303 post :unarchive, :id => 1
304 assert_redirected_to 'admin/projects'
304 assert_redirected_to 'admin/projects'
305 assert Project.find(1).active?
305 assert Project.find(1).active?
306 end
306 end
307 end
307 end
@@ -1,65 +1,66
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "#{File.dirname(__FILE__)}/../test_helper"
18 require "#{File.dirname(__FILE__)}/../test_helper"
19
19
20 class AdminTest < ActionController::IntegrationTest
20 class AdminTest < ActionController::IntegrationTest
21 fixtures :users
21 fixtures :users
22
22
23 def test_add_user
23 def test_add_user
24 log_user("admin", "admin")
24 log_user("admin", "admin")
25 get "/users/add"
25 get "/users/add"
26 assert_response :success
26 assert_response :success
27 assert_template "users/add"
27 assert_template "users/add"
28 post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
28 post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
29 assert_redirected_to "users/list"
29 assert_redirected_to "users/list"
30
30
31 user = User.find_by_login("psmith")
31 user = User.find_by_login("psmith")
32 assert_kind_of User, user
32 assert_kind_of User, user
33 logged_user = User.try_to_login("psmith", "psmith09")
33 logged_user = User.try_to_login("psmith", "psmith09")
34 assert_kind_of User, logged_user
34 assert_kind_of User, logged_user
35 assert_equal "Paul", logged_user.firstname
35 assert_equal "Paul", logged_user.firstname
36
36
37 post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
37 post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
38 assert_redirected_to "users/list"
38 assert_redirected_to "users/list"
39 locked_user = User.try_to_login("psmith", "psmith09")
39 locked_user = User.try_to_login("psmith", "psmith09")
40 assert_equal nil, locked_user
40 assert_equal nil, locked_user
41 end
41 end
42
42
43 def test_add_project
43 def test_add_project
44 log_user("admin", "admin")
44 log_user("admin", "admin")
45 get "projects/add"
45 get "projects/add"
46 assert_response :success
46 assert_response :success
47 assert_template "projects/add"
47 assert_template "projects/add"
48 post "projects/add", :project => { :name => "blog",
48 post "projects/add", :project => { :name => "blog",
49 :description => "weblog",
49 :description => "weblog",
50 :identifier => "blog",
50 :identifier => "blog",
51 :is_public => 1 },
51 :is_public => 1,
52 'custom_fields[3]' => 'Beta'
52 :custom_field_values => { '3' => 'Beta' }
53 }
53 assert_redirected_to "admin/projects"
54 assert_redirected_to "admin/projects"
54 assert_equal 'Successful creation.', flash[:notice]
55 assert_equal 'Successful creation.', flash[:notice]
55
56
56 project = Project.find_by_name("blog")
57 project = Project.find_by_name("blog")
57 assert_kind_of Project, project
58 assert_kind_of Project, project
58 assert_equal "weblog", project.description
59 assert_equal "weblog", project.description
59 assert_equal true, project.is_public?
60 assert_equal true, project.is_public?
60
61
61 get "admin/projects"
62 get "admin/projects"
62 assert_response :success
63 assert_response :success
63 assert_template "admin/projects"
64 assert_template "admin/projects"
64 end
65 end
65 end
66 end
@@ -1,108 +1,184
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
21 fixtures :projects, :users, :members,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories,
24 :enumerations,
25 :issues,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :time_entries
22
28
23 def test_create
29 def test_create
24 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
25 assert issue.save
31 assert issue.save
26 issue.reload
32 issue.reload
27 assert_equal 1.5, issue.estimated_hours
33 assert_equal 1.5, issue.estimated_hours
28 end
34 end
29
35
36 def test_create_with_required_custom_field
37 field = IssueCustomField.find_by_name('Database')
38 field.update_attribute(:is_required, true)
39
40 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
41 assert issue.available_custom_fields.include?(field)
42 # No value for the custom field
43 assert !issue.save
44 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
45 # Blank value
46 issue.custom_field_values = { field.id => '' }
47 assert !issue.save
48 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
49 # Invalid value
50 issue.custom_field_values = { field.id => 'SQLServer' }
51 assert !issue.save
52 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
53 # Valid value
54 issue.custom_field_values = { field.id => 'PostgreSQL' }
55 assert issue.save
56 issue.reload
57 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
58 end
59
60 def test_update_issue_with_required_custom_field
61 field = IssueCustomField.find_by_name('Database')
62 field.update_attribute(:is_required, true)
63
64 issue = Issue.find(1)
65 assert_nil issue.custom_value_for(field)
66 assert issue.available_custom_fields.include?(field)
67 # No change to custom values, issue can be saved
68 assert issue.save
69 # Blank value
70 issue.custom_field_values = { field.id => '' }
71 assert !issue.save
72 # Valid value
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 assert issue.save
75 issue.reload
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 end
78
79 def test_should_not_update_attributes_if_custom_fields_validation_fails
80 issue = Issue.find(1)
81 field = IssueCustomField.find_by_name('Database')
82 assert issue.available_custom_fields.include?(field)
83
84 issue.custom_field_values = { field.id => 'Invalid' }
85 issue.subject = 'Should be not be saved'
86 assert !issue.save
87
88 issue.reload
89 assert_equal "Can't print recipes", issue.subject
90 end
91
92 def test_should_not_recreate_custom_values_objects_on_update
93 field = IssueCustomField.find_by_name('Database')
94
95 issue = Issue.find(1)
96 issue.custom_field_values = { field.id => 'PostgreSQL' }
97 assert issue.save
98 custom_value = issue.custom_value_for(field)
99 issue.reload
100 issue.custom_field_values = { field.id => 'MySQL' }
101 assert issue.save
102 issue.reload
103 assert_equal custom_value.id, issue.custom_value_for(field).id
104 end
105
30 def test_category_based_assignment
106 def test_category_based_assignment
31 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
107 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
32 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
108 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
33 end
109 end
34
110
35 def test_copy
111 def test_copy
36 issue = Issue.new.copy_from(1)
112 issue = Issue.new.copy_from(1)
37 assert issue.save
113 assert issue.save
38 issue.reload
114 issue.reload
39 orig = Issue.find(1)
115 orig = Issue.find(1)
40 assert_equal orig.subject, issue.subject
116 assert_equal orig.subject, issue.subject
41 assert_equal orig.tracker, issue.tracker
117 assert_equal orig.tracker, issue.tracker
42 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
118 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
43 end
119 end
44
120
45 def test_should_close_duplicates
121 def test_should_close_duplicates
46 # Create 3 issues
122 # Create 3 issues
47 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
123 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
48 assert issue1.save
124 assert issue1.save
49 issue2 = issue1.clone
125 issue2 = issue1.clone
50 assert issue2.save
126 assert issue2.save
51 issue3 = issue1.clone
127 issue3 = issue1.clone
52 assert issue3.save
128 assert issue3.save
53
129
54 # 2 is a dupe of 1
130 # 2 is a dupe of 1
55 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
131 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
56 # And 3 is a dupe of 2
132 # And 3 is a dupe of 2
57 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
133 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
58 # And 3 is a dupe of 1 (circular duplicates)
134 # And 3 is a dupe of 1 (circular duplicates)
59 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
135 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
60
136
61 assert issue1.reload.duplicates.include?(issue2)
137 assert issue1.reload.duplicates.include?(issue2)
62
138
63 # Closing issue 1
139 # Closing issue 1
64 issue1.init_journal(User.find(:first), "Closing issue1")
140 issue1.init_journal(User.find(:first), "Closing issue1")
65 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
141 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
66 assert issue1.save
142 assert issue1.save
67 # 2 and 3 should be also closed
143 # 2 and 3 should be also closed
68 assert issue2.reload.closed?
144 assert issue2.reload.closed?
69 assert issue3.reload.closed?
145 assert issue3.reload.closed?
70 end
146 end
71
147
72 def test_should_not_close_duplicated_issue
148 def test_should_not_close_duplicated_issue
73 # Create 3 issues
149 # Create 3 issues
74 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
150 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
75 assert issue1.save
151 assert issue1.save
76 issue2 = issue1.clone
152 issue2 = issue1.clone
77 assert issue2.save
153 assert issue2.save
78
154
79 # 2 is a dupe of 1
155 # 2 is a dupe of 1
80 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
156 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
81 # 2 is a dup of 1 but 1 is not a duplicate of 2
157 # 2 is a dup of 1 but 1 is not a duplicate of 2
82 assert !issue2.reload.duplicates.include?(issue1)
158 assert !issue2.reload.duplicates.include?(issue1)
83
159
84 # Closing issue 2
160 # Closing issue 2
85 issue2.init_journal(User.find(:first), "Closing issue2")
161 issue2.init_journal(User.find(:first), "Closing issue2")
86 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
162 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
87 assert issue2.save
163 assert issue2.save
88 # 1 should not be also closed
164 # 1 should not be also closed
89 assert !issue1.reload.closed?
165 assert !issue1.reload.closed?
90 end
166 end
91
167
92 def test_move_to_another_project
168 def test_move_to_another_project
93 issue = Issue.find(1)
169 issue = Issue.find(1)
94 assert issue.move_to(Project.find(2))
170 assert issue.move_to(Project.find(2))
95 issue.reload
171 issue.reload
96 assert_equal 2, issue.project_id
172 assert_equal 2, issue.project_id
97 # Category removed
173 # Category removed
98 assert_nil issue.category
174 assert_nil issue.category
99 # Make sure time entries were move to the target project
175 # Make sure time entries were move to the target project
100 assert_equal 2, issue.time_entries.first.project_id
176 assert_equal 2, issue.time_entries.first.project_id
101 end
177 end
102
178
103 def test_issue_destroy
179 def test_issue_destroy
104 Issue.find(1).destroy
180 Issue.find(1).destroy
105 assert_nil Issue.find_by_id(1)
181 assert_nil Issue.find_by_id(1)
106 assert_nil TimeEntry.find_by_issue_id(1)
182 assert_nil TimeEntry.find_by_issue_id(1)
107 end
183 end
108 end
184 end
General Comments 0
You need to be logged in to leave comments. Login now