##// END OF EJS Templates
Fixes custom fields display order at several places (#1768)....
Jean-Philippe Lang -
r1730:237a3f52a2d9
parent child
Show More
@@ -1,190 +1,190
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 helper :custom_fields
19 helper :custom_fields
20 include CustomFieldsHelper
20 include CustomFieldsHelper
21
21
22 # prevents login action to be filtered by check_if_login_required application scope filter
22 # prevents login action to be filtered by check_if_login_required application scope filter
23 skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
23 skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
24
24
25 # Show user's account
25 # Show user's account
26 def show
26 def show
27 @user = User.find_active(params[:id])
27 @user = User.find_active(params[:id])
28 @custom_values = @user.custom_values.find(:all, :include => :custom_field)
28 @custom_values = @user.custom_values
29
29
30 # show only public projects and private projects that the logged in user is also a member of
30 # show only public projects and private projects that the logged in user is also a member of
31 @memberships = @user.memberships.select do |membership|
31 @memberships = @user.memberships.select do |membership|
32 membership.project.is_public? || (User.current.member_of?(membership.project))
32 membership.project.is_public? || (User.current.member_of?(membership.project))
33 end
33 end
34 rescue ActiveRecord::RecordNotFound
34 rescue ActiveRecord::RecordNotFound
35 render_404
35 render_404
36 end
36 end
37
37
38 # Login request and validation
38 # Login request and validation
39 def login
39 def login
40 if request.get?
40 if request.get?
41 # Logout user
41 # Logout user
42 self.logged_user = nil
42 self.logged_user = nil
43 else
43 else
44 # Authenticate user
44 # Authenticate user
45 user = User.try_to_login(params[:username], params[:password])
45 user = User.try_to_login(params[:username], params[:password])
46 if user.nil?
46 if user.nil?
47 # Invalid credentials
47 # Invalid credentials
48 flash.now[:error] = l(:notice_account_invalid_creditentials)
48 flash.now[:error] = l(:notice_account_invalid_creditentials)
49 elsif user.new_record?
49 elsif user.new_record?
50 # Onthefly creation failed, display the registration form to fill/fix attributes
50 # Onthefly creation failed, display the registration form to fill/fix attributes
51 @user = user
51 @user = user
52 session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
52 session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
53 render :action => 'register'
53 render :action => 'register'
54 else
54 else
55 # Valid user
55 # Valid user
56 self.logged_user = user
56 self.logged_user = user
57 # generate a key and set cookie if autologin
57 # generate a key and set cookie if autologin
58 if params[:autologin] && Setting.autologin?
58 if params[:autologin] && Setting.autologin?
59 token = Token.create(:user => user, :action => 'autologin')
59 token = Token.create(:user => user, :action => 'autologin')
60 cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
60 cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
61 end
61 end
62 redirect_back_or_default :controller => 'my', :action => 'page'
62 redirect_back_or_default :controller => 'my', :action => 'page'
63 end
63 end
64 end
64 end
65 end
65 end
66
66
67 # Log out current user and redirect to welcome page
67 # Log out current user and redirect to welcome page
68 def logout
68 def logout
69 cookies.delete :autologin
69 cookies.delete :autologin
70 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
70 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
71 self.logged_user = nil
71 self.logged_user = nil
72 redirect_to home_url
72 redirect_to home_url
73 end
73 end
74
74
75 # Enable user to choose a new password
75 # Enable user to choose a new password
76 def lost_password
76 def lost_password
77 redirect_to(home_url) && return unless Setting.lost_password?
77 redirect_to(home_url) && return unless Setting.lost_password?
78 if params[:token]
78 if params[:token]
79 @token = Token.find_by_action_and_value("recovery", params[:token])
79 @token = Token.find_by_action_and_value("recovery", params[:token])
80 redirect_to(home_url) && return unless @token and !@token.expired?
80 redirect_to(home_url) && return unless @token and !@token.expired?
81 @user = @token.user
81 @user = @token.user
82 if request.post?
82 if request.post?
83 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
83 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
84 if @user.save
84 if @user.save
85 @token.destroy
85 @token.destroy
86 flash[:notice] = l(:notice_account_password_updated)
86 flash[:notice] = l(:notice_account_password_updated)
87 redirect_to :action => 'login'
87 redirect_to :action => 'login'
88 return
88 return
89 end
89 end
90 end
90 end
91 render :template => "account/password_recovery"
91 render :template => "account/password_recovery"
92 return
92 return
93 else
93 else
94 if request.post?
94 if request.post?
95 user = User.find_by_mail(params[:mail])
95 user = User.find_by_mail(params[:mail])
96 # user not found in db
96 # user not found in db
97 flash.now[:error] = l(:notice_account_unknown_email) and return unless user
97 flash.now[:error] = l(:notice_account_unknown_email) and return unless user
98 # user uses an external authentification
98 # user uses an external authentification
99 flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
99 flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
100 # create a new token for password recovery
100 # create a new token for password recovery
101 token = Token.new(:user => user, :action => "recovery")
101 token = Token.new(:user => user, :action => "recovery")
102 if token.save
102 if token.save
103 Mailer.deliver_lost_password(token)
103 Mailer.deliver_lost_password(token)
104 flash[:notice] = l(:notice_account_lost_email_sent)
104 flash[:notice] = l(:notice_account_lost_email_sent)
105 redirect_to :action => 'login'
105 redirect_to :action => 'login'
106 return
106 return
107 end
107 end
108 end
108 end
109 end
109 end
110 end
110 end
111
111
112 # User self-registration
112 # User self-registration
113 def register
113 def register
114 redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
114 redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
115 if request.get?
115 if request.get?
116 session[:auth_source_registration] = nil
116 session[:auth_source_registration] = nil
117 @user = User.new(:language => Setting.default_language)
117 @user = User.new(:language => Setting.default_language)
118 else
118 else
119 @user = User.new(params[:user])
119 @user = User.new(params[:user])
120 @user.admin = false
120 @user.admin = false
121 @user.status = User::STATUS_REGISTERED
121 @user.status = User::STATUS_REGISTERED
122 if session[:auth_source_registration]
122 if session[:auth_source_registration]
123 @user.status = User::STATUS_ACTIVE
123 @user.status = User::STATUS_ACTIVE
124 @user.login = session[:auth_source_registration][:login]
124 @user.login = session[:auth_source_registration][:login]
125 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
125 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
126 if @user.save
126 if @user.save
127 session[:auth_source_registration] = nil
127 session[:auth_source_registration] = nil
128 self.logged_user = @user
128 self.logged_user = @user
129 flash[:notice] = l(:notice_account_activated)
129 flash[:notice] = l(:notice_account_activated)
130 redirect_to :controller => 'my', :action => 'account'
130 redirect_to :controller => 'my', :action => 'account'
131 end
131 end
132 else
132 else
133 @user.login = params[:user][:login]
133 @user.login = params[:user][:login]
134 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
134 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
135 case Setting.self_registration
135 case Setting.self_registration
136 when '1'
136 when '1'
137 # Email activation
137 # Email activation
138 token = Token.new(:user => @user, :action => "register")
138 token = Token.new(:user => @user, :action => "register")
139 if @user.save and token.save
139 if @user.save and token.save
140 Mailer.deliver_register(token)
140 Mailer.deliver_register(token)
141 flash[:notice] = l(:notice_account_register_done)
141 flash[:notice] = l(:notice_account_register_done)
142 redirect_to :action => 'login'
142 redirect_to :action => 'login'
143 end
143 end
144 when '3'
144 when '3'
145 # Automatic activation
145 # Automatic activation
146 @user.status = User::STATUS_ACTIVE
146 @user.status = User::STATUS_ACTIVE
147 if @user.save
147 if @user.save
148 self.logged_user = @user
148 self.logged_user = @user
149 flash[:notice] = l(:notice_account_activated)
149 flash[:notice] = l(:notice_account_activated)
150 redirect_to :controller => 'my', :action => 'account'
150 redirect_to :controller => 'my', :action => 'account'
151 end
151 end
152 else
152 else
153 # Manual activation by the administrator
153 # Manual activation by the administrator
154 if @user.save
154 if @user.save
155 # Sends an email to the administrators
155 # Sends an email to the administrators
156 Mailer.deliver_account_activation_request(@user)
156 Mailer.deliver_account_activation_request(@user)
157 flash[:notice] = l(:notice_account_pending)
157 flash[:notice] = l(:notice_account_pending)
158 redirect_to :action => 'login'
158 redirect_to :action => 'login'
159 end
159 end
160 end
160 end
161 end
161 end
162 end
162 end
163 end
163 end
164
164
165 # Token based account activation
165 # Token based account activation
166 def activate
166 def activate
167 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
167 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
168 token = Token.find_by_action_and_value('register', params[:token])
168 token = Token.find_by_action_and_value('register', params[:token])
169 redirect_to(home_url) && return unless token and !token.expired?
169 redirect_to(home_url) && return unless token and !token.expired?
170 user = token.user
170 user = token.user
171 redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
171 redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
172 user.status = User::STATUS_ACTIVE
172 user.status = User::STATUS_ACTIVE
173 if user.save
173 if user.save
174 token.destroy
174 token.destroy
175 flash[:notice] = l(:notice_account_activated)
175 flash[:notice] = l(:notice_account_activated)
176 end
176 end
177 redirect_to :action => 'login'
177 redirect_to :action => 'login'
178 end
178 end
179
179
180 private
180 private
181 def logged_user=(user)
181 def logged_user=(user)
182 if user && user.is_a?(User)
182 if user && user.is_a?(User)
183 User.current = user
183 User.current = user
184 session[:user_id] = user.id
184 session[:user_id] = user.id
185 else
185 else
186 User.current = User.anonymous
186 User.current = User.anonymous
187 session[:user_id] = nil
187 session[:user_id] = nil
188 end
188 end
189 end
189 end
190 end
190 end
@@ -1,75 +1,75
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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 has_many :custom_values, :dependent => :delete_all
19 has_many :custom_values, :dependent => :delete_all
20 acts_as_list :scope => 'type = \'#{self.class}\''
20 acts_as_list :scope => 'type = \'#{self.class}\''
21 serialize :possible_values
21 serialize :possible_values
22
22
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
24 "text" => { :name => :label_text, :order => 2 },
24 "text" => { :name => :label_text, :order => 2 },
25 "int" => { :name => :label_integer, :order => 3 },
25 "int" => { :name => :label_integer, :order => 3 },
26 "float" => { :name => :label_float, :order => 4 },
26 "float" => { :name => :label_float, :order => 4 },
27 "list" => { :name => :label_list, :order => 5 },
27 "list" => { :name => :label_list, :order => 5 },
28 "date" => { :name => :label_date, :order => 6 },
28 "date" => { :name => :label_date, :order => 6 },
29 "bool" => { :name => :label_boolean, :order => 7 }
29 "bool" => { :name => :label_boolean, :order => 7 }
30 }.freeze
30 }.freeze
31
31
32 validates_presence_of :name, :field_format
32 validates_presence_of :name, :field_format
33 validates_uniqueness_of :name, :scope => :type
33 validates_uniqueness_of :name, :scope => :type
34 validates_length_of :name, :maximum => 30
34 validates_length_of :name, :maximum => 30
35 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
35 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
37
37
38 def initialize(attributes = nil)
38 def initialize(attributes = nil)
39 super
39 super
40 self.possible_values ||= []
40 self.possible_values ||= []
41 end
41 end
42
42
43 def before_validation
43 def before_validation
44 # remove empty values
44 # remove empty values
45 self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
45 self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
46 # make sure these fields are not searchable
46 # make sure these fields are not searchable
47 self.searchable = false if %w(int float date bool).include?(field_format)
47 self.searchable = false if %w(int float date bool).include?(field_format)
48 true
48 true
49 end
49 end
50
50
51 def validate
51 def validate
52 if self.field_format == "list"
52 if self.field_format == "list"
53 errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
53 errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
54 errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
54 errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
55 end
55 end
56
56
57 # validate default value
57 # validate default value
58 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
58 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
59 v.custom_field.is_required = false
59 v.custom_field.is_required = false
60 errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
60 errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
61 end
61 end
62
62
63 def <=>(field)
63 def <=>(field)
64 position <=> field.position
64 position <=> field.position
65 end
65 end
66
66
67 # to move in project_custom_field
67 # to move in project_custom_field
68 def self.for_all
68 def self.for_all
69 find(:all, :conditions => ["is_for_all=?", true])
69 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
70 end
70 end
71
71
72 def type_name
72 def type_name
73 nil
73 nil
74 end
74 end
75 end
75 end
@@ -1,260 +1,260
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 :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 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]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 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"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 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"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_tree :order => "name", :counter_cache => true
46 acts_as_tree :order => "name", :counter_cache => true
47
47
48 acts_as_customizable
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 :author => nil
52 :author => nil
53
53
54 attr_protected :status, :enabled_module_names
54 attr_protected :status, :enabled_module_names
55
55
56 validates_presence_of :name, :identifier
56 validates_presence_of :name, :identifier
57 validates_uniqueness_of :name, :identifier
57 validates_uniqueness_of :name, :identifier
58 validates_associated :repository, :wiki
58 validates_associated :repository, :wiki
59 validates_length_of :name, :maximum => 30
59 validates_length_of :name, :maximum => 30
60 validates_length_of :homepage, :maximum => 255
60 validates_length_of :homepage, :maximum => 255
61 validates_length_of :identifier, :in => 3..20
61 validates_length_of :identifier, :in => 3..20
62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
63
63
64 before_destroy :delete_all_members
64 before_destroy :delete_all_members
65
65
66 def identifier=(identifier)
66 def identifier=(identifier)
67 super unless identifier_frozen?
67 super unless identifier_frozen?
68 end
68 end
69
69
70 def identifier_frozen?
70 def identifier_frozen?
71 errors[:identifier].nil? && !(new_record? || identifier.blank?)
71 errors[:identifier].nil? && !(new_record? || identifier.blank?)
72 end
72 end
73
73
74 def issues_with_subprojects(include_subprojects=false)
74 def issues_with_subprojects(include_subprojects=false)
75 conditions = nil
75 conditions = nil
76 if include_subprojects
76 if include_subprojects
77 ids = [id] + child_ids
77 ids = [id] + child_ids
78 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
78 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
79 end
79 end
80 conditions ||= ["#{Project.table_name}.id = ?", id]
80 conditions ||= ["#{Project.table_name}.id = ?", id]
81 # Quick and dirty fix for Rails 2 compatibility
81 # Quick and dirty fix for Rails 2 compatibility
82 Issue.send(:with_scope, :find => { :conditions => conditions }) do
82 Issue.send(:with_scope, :find => { :conditions => conditions }) do
83 Version.send(:with_scope, :find => { :conditions => conditions }) do
83 Version.send(:with_scope, :find => { :conditions => conditions }) do
84 yield
84 yield
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 # returns latest created projects
89 # returns latest created projects
90 # non public projects will be returned only if user is a member of those
90 # non public projects will be returned only if user is a member of those
91 def self.latest(user=nil, count=5)
91 def self.latest(user=nil, count=5)
92 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
92 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
93 end
93 end
94
94
95 def self.visible_by(user=nil)
95 def self.visible_by(user=nil)
96 user ||= User.current
96 user ||= User.current
97 if user && user.admin?
97 if user && user.admin?
98 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
98 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
99 elsif user && user.memberships.any?
99 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(',')}))"
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(',')}))"
101 else
101 else
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
103 end
103 end
104 end
104 end
105
105
106 def self.allowed_to_condition(user, permission, options={})
106 def self.allowed_to_condition(user, permission, options={})
107 statements = []
107 statements = []
108 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
108 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
109 if options[:project]
109 if options[:project]
110 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
110 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]
111 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
112 base_statement = "(#{project_statement}) AND (#{base_statement})"
112 base_statement = "(#{project_statement}) AND (#{base_statement})"
113 end
113 end
114 if user.admin?
114 if user.admin?
115 # no restriction
115 # no restriction
116 else
116 else
117 statements << "1=0"
117 statements << "1=0"
118 if user.logged?
118 if user.logged?
119 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
119 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
120 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
120 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
121 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
121 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
122 elsif Role.anonymous.allowed_to?(permission)
122 elsif Role.anonymous.allowed_to?(permission)
123 # anonymous user allowed on public project
123 # anonymous user allowed on public project
124 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
124 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
125 else
125 else
126 # anonymous user is not authorized
126 # anonymous user is not authorized
127 end
127 end
128 end
128 end
129 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
129 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
130 end
130 end
131
131
132 def project_condition(with_subprojects)
132 def project_condition(with_subprojects)
133 cond = "#{Project.table_name}.id = #{id}"
133 cond = "#{Project.table_name}.id = #{id}"
134 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
134 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
135 cond
135 cond
136 end
136 end
137
137
138 def self.find(*args)
138 def self.find(*args)
139 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
139 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
140 project = find_by_identifier(*args)
140 project = find_by_identifier(*args)
141 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
141 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
142 project
142 project
143 else
143 else
144 super
144 super
145 end
145 end
146 end
146 end
147
147
148 def to_param
148 def to_param
149 # id is used for projects with a numeric identifier (compatibility)
149 # id is used for projects with a numeric identifier (compatibility)
150 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
150 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
151 end
151 end
152
152
153 def active?
153 def active?
154 self.status == STATUS_ACTIVE
154 self.status == STATUS_ACTIVE
155 end
155 end
156
156
157 def archive
157 def archive
158 # Archive subprojects if any
158 # Archive subprojects if any
159 children.each do |subproject|
159 children.each do |subproject|
160 subproject.archive
160 subproject.archive
161 end
161 end
162 update_attribute :status, STATUS_ARCHIVED
162 update_attribute :status, STATUS_ARCHIVED
163 end
163 end
164
164
165 def unarchive
165 def unarchive
166 return false if parent && !parent.active?
166 return false if parent && !parent.active?
167 update_attribute :status, STATUS_ACTIVE
167 update_attribute :status, STATUS_ACTIVE
168 end
168 end
169
169
170 def active_children
170 def active_children
171 children.select {|child| child.active?}
171 children.select {|child| child.active?}
172 end
172 end
173
173
174 # Returns an array of the trackers used by the project and its sub projects
174 # Returns an array of the trackers used by the project and its sub projects
175 def rolled_up_trackers
175 def rolled_up_trackers
176 @rolled_up_trackers ||=
176 @rolled_up_trackers ||=
177 Tracker.find(:all, :include => :projects,
177 Tracker.find(:all, :include => :projects,
178 :select => "DISTINCT #{Tracker.table_name}.*",
178 :select => "DISTINCT #{Tracker.table_name}.*",
179 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
179 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
180 :order => "#{Tracker.table_name}.position")
180 :order => "#{Tracker.table_name}.position")
181 end
181 end
182
182
183 # Deletes all project's members
183 # Deletes all project's members
184 def delete_all_members
184 def delete_all_members
185 Member.delete_all(['project_id = ?', id])
185 Member.delete_all(['project_id = ?', id])
186 end
186 end
187
187
188 # Users issues can be assigned to
188 # Users issues can be assigned to
189 def assignable_users
189 def assignable_users
190 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
190 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
191 end
191 end
192
192
193 # Returns the mail adresses of users that should be always notified on project events
193 # Returns the mail adresses of users that should be always notified on project events
194 def recipients
194 def recipients
195 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
195 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
196 end
196 end
197
197
198 # Returns an array of all custom fields enabled for project issues
198 # Returns an array of all custom fields enabled for project issues
199 # (explictly associated custom fields and custom fields enabled for all projects)
199 # (explictly associated custom fields and custom fields enabled for all projects)
200 def all_issue_custom_fields
200 def all_issue_custom_fields
201 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq
201 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
202 end
202 end
203
203
204 def project
204 def project
205 self
205 self
206 end
206 end
207
207
208 def <=>(project)
208 def <=>(project)
209 name.downcase <=> project.name.downcase
209 name.downcase <=> project.name.downcase
210 end
210 end
211
211
212 def to_s
212 def to_s
213 name
213 name
214 end
214 end
215
215
216 # Returns a short description of the projects (first lines)
216 # Returns a short description of the projects (first lines)
217 def short_description(length = 255)
217 def short_description(length = 255)
218 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
218 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
219 end
219 end
220
220
221 def allows_to?(action)
221 def allows_to?(action)
222 if action.is_a? Hash
222 if action.is_a? Hash
223 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
223 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
224 else
224 else
225 allowed_permissions.include? action
225 allowed_permissions.include? action
226 end
226 end
227 end
227 end
228
228
229 def module_enabled?(module_name)
229 def module_enabled?(module_name)
230 module_name = module_name.to_s
230 module_name = module_name.to_s
231 enabled_modules.detect {|m| m.name == module_name}
231 enabled_modules.detect {|m| m.name == module_name}
232 end
232 end
233
233
234 def enabled_module_names=(module_names)
234 def enabled_module_names=(module_names)
235 enabled_modules.clear
235 enabled_modules.clear
236 module_names = [] unless module_names && module_names.is_a?(Array)
236 module_names = [] unless module_names && module_names.is_a?(Array)
237 module_names.each do |name|
237 module_names.each do |name|
238 enabled_modules << EnabledModule.new(:name => name.to_s)
238 enabled_modules << EnabledModule.new(:name => name.to_s)
239 end
239 end
240 end
240 end
241
241
242 protected
242 protected
243 def validate
243 def validate
244 errors.add(parent_id, " must be a root project") if parent and parent.parent
244 errors.add(parent_id, " must be a root project") if parent and parent.parent
245 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
245 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
246 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
246 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
247 end
247 end
248
248
249 private
249 private
250 def allowed_permissions
250 def allowed_permissions
251 @allowed_permissions ||= begin
251 @allowed_permissions ||= begin
252 module_names = enabled_modules.collect {|m| m.name}
252 module_names = enabled_modules.collect {|m| m.name}
253 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
253 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
254 end
254 end
255 end
255 end
256
256
257 def allowed_actions
257 def allowed_actions
258 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
258 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
259 end
259 end
260 end
260 end
@@ -1,51 +1,51
1 <% if @issue.new_record? %>
1 <% if @issue.new_record? %>
2 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
2 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
3 <%= observe_field :issue_tracker_id, :url => { :action => :new },
3 <%= observe_field :issue_tracker_id, :url => { :action => :new },
4 :update => :content,
4 :update => :content,
5 :with => "Form.serialize('issue-form')" %>
5 :with => "Form.serialize('issue-form')" %>
6 <hr />
6 <hr />
7 <% end %>
7 <% end %>
8
8
9 <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
9 <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
10 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
10 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
11 <p><%= f.text_area :description, :required => true,
11 <p><%= f.text_area :description, :required => true,
12 :cols => 60,
12 :cols => 60,
13 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
13 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
14 :accesskey => accesskey(:edit),
14 :accesskey => accesskey(:edit),
15 :class => 'wiki-edit' %></p>
15 :class => 'wiki-edit' %></p>
16 </div>
16 </div>
17
17
18 <div class="splitcontentleft">
18 <div class="splitcontentleft">
19 <% if @issue.new_record? || @allowed_statuses.any? %>
19 <% if @issue.new_record? || @allowed_statuses.any? %>
20 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
20 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
21 <% else %>
21 <% else %>
22 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
22 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
23 <% end %>
23 <% end %>
24
24
25 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
25 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
26 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
26 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
27 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
27 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
28 <%= prompt_to_remote(l(:label_issue_category_new),
28 <%= prompt_to_remote(l(:label_issue_category_new),
29 l(:label_issue_category_new), 'category[name]',
29 l(:label_issue_category_new), 'category[name]',
30 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
30 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
31 :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
31 :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
32 <%= content_tag('p', f.select(:fixed_version_id,
32 <%= content_tag('p', f.select(:fixed_version_id,
33 (@project.versions.sort.collect {|v| [v.name, v.id]}),
33 (@project.versions.sort.collect {|v| [v.name, v.id]}),
34 { :include_blank => true })) unless @project.versions.empty? %>
34 { :include_blank => true })) unless @project.versions.empty? %>
35 </div>
35 </div>
36
36
37 <div class="splitcontentright">
37 <div class="splitcontentright">
38 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
38 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
39 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
39 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
40 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
40 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
41 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
41 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
42 </div>
42 </div>
43
43
44 <div style="clear:both;"> </div>
44 <div style="clear:both;"> </div>
45 <%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %>
45 <%= render :partial => 'form_custom_fields' %>
46
46
47 <% if @issue.new_record? %>
47 <% if @issue.new_record? %>
48 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
48 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
49 <% end %>
49 <% end %>
50
50
51 <%= wikitoolbar_for 'issue_description' %>
51 <%= wikitoolbar_for 'issue_description' %>
@@ -1,12 +1,12
1 <div class="splitcontentleft">
1 <div class="splitcontentleft">
2 <% i = 1 %>
2 <% i = 0 %>
3 <% split_on = @issue.custom_field_values.size / 2 %>
3 <% split_on = @issue.custom_field_values.size / 2 %>
4 <% @issue.custom_field_values.each do |value| %>
4 <% @issue.custom_field_values.each do |value| %>
5 <p><%= custom_field_tag_with_label :issue, value %></p>
5 <p><%= custom_field_tag_with_label :issue, value %></p>
6 <% if i == split_on -%>
6 <% if i == split_on -%>
7 </div><div class="splitcontentright">
7 </div><div class="splitcontentright">
8 <% end -%>
8 <% end -%>
9 <% i += 1 -%>
9 <% i += 1 -%>
10 <% end -%>
10 <% end -%>
11 </div>
11 </div>
12 <div style="clear:both;"> </div>
12 <div style="clear:both;"> </div>
@@ -1,641 +1,641
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 => 'issue[custom_field_values][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 => 3,
202 :issue => {:tracker_id => 3,
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_field_values => {'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_equal 3, issue.tracker_id
213 assert_equal 3, issue.tracker_id
214 assert_nil issue.estimated_hours
214 assert_nil issue.estimated_hours
215 v = issue.custom_values.find_by_custom_field_id(2)
215 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
216 assert_not_nil v
216 assert_not_nil v
217 assert_equal 'Value for field 2', v.value
217 assert_equal 'Value for field 2', v.value
218 end
218 end
219
219
220 def test_post_new_without_custom_fields_param
220 def test_post_new_without_custom_fields_param
221 @request.session[:user_id] = 2
221 @request.session[:user_id] = 2
222 post :new, :project_id => 1,
222 post :new, :project_id => 1,
223 :issue => {:tracker_id => 1,
223 :issue => {:tracker_id => 1,
224 :subject => 'This is the test_new issue',
224 :subject => 'This is the test_new issue',
225 :description => 'This is the description',
225 :description => 'This is the description',
226 :priority_id => 5}
226 :priority_id => 5}
227 assert_redirected_to 'issues/show'
227 assert_redirected_to 'issues/show'
228 end
228 end
229
229
230 def test_post_new_with_required_custom_field_and_without_custom_fields_param
230 def test_post_new_with_required_custom_field_and_without_custom_fields_param
231 field = IssueCustomField.find_by_name('Database')
231 field = IssueCustomField.find_by_name('Database')
232 field.update_attribute(:is_required, true)
232 field.update_attribute(:is_required, true)
233
233
234 @request.session[:user_id] = 2
234 @request.session[:user_id] = 2
235 post :new, :project_id => 1,
235 post :new, :project_id => 1,
236 :issue => {:tracker_id => 1,
236 :issue => {:tracker_id => 1,
237 :subject => 'This is the test_new issue',
237 :subject => 'This is the test_new issue',
238 :description => 'This is the description',
238 :description => 'This is the description',
239 :priority_id => 5}
239 :priority_id => 5}
240 assert_response :success
240 assert_response :success
241 assert_template 'new'
241 assert_template 'new'
242 issue = assigns(:issue)
242 issue = assigns(:issue)
243 assert_not_nil issue
243 assert_not_nil issue
244 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
244 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
245 end
245 end
246
246
247 def test_post_should_preserve_fields_values_on_validation_failure
247 def test_post_should_preserve_fields_values_on_validation_failure
248 @request.session[:user_id] = 2
248 @request.session[:user_id] = 2
249 post :new, :project_id => 1,
249 post :new, :project_id => 1,
250 :issue => {:tracker_id => 1,
250 :issue => {:tracker_id => 1,
251 :subject => 'This is the test_new issue',
251 :subject => 'This is the test_new issue',
252 # empty description
252 # empty description
253 :description => '',
253 :description => '',
254 :priority_id => 6,
254 :priority_id => 6,
255 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
255 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
256 assert_response :success
256 assert_response :success
257 assert_template 'new'
257 assert_template 'new'
258
258
259 assert_tag :input, :attributes => { :name => 'issue[subject]',
259 assert_tag :input, :attributes => { :name => 'issue[subject]',
260 :value => 'This is the test_new issue' }
260 :value => 'This is the test_new issue' }
261 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
261 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
262 :child => { :tag => 'option', :attributes => { :selected => 'selected',
262 :child => { :tag => 'option', :attributes => { :selected => 'selected',
263 :value => '6' },
263 :value => '6' },
264 :content => 'High' }
264 :content => 'High' }
265 # Custom fields
265 # Custom fields
266 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
266 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
267 :child => { :tag => 'option', :attributes => { :selected => 'selected',
267 :child => { :tag => 'option', :attributes => { :selected => 'selected',
268 :value => 'Oracle' },
268 :value => 'Oracle' },
269 :content => 'Oracle' }
269 :content => 'Oracle' }
270 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
270 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
271 :value => 'Value for field 2'}
271 :value => 'Value for field 2'}
272 end
272 end
273
273
274 def test_copy_issue
274 def test_copy_issue
275 @request.session[:user_id] = 2
275 @request.session[:user_id] = 2
276 get :new, :project_id => 1, :copy_from => 1
276 get :new, :project_id => 1, :copy_from => 1
277 assert_template 'new'
277 assert_template 'new'
278 assert_not_nil assigns(:issue)
278 assert_not_nil assigns(:issue)
279 orig = Issue.find(1)
279 orig = Issue.find(1)
280 assert_equal orig.subject, assigns(:issue).subject
280 assert_equal orig.subject, assigns(:issue).subject
281 end
281 end
282
282
283 def test_get_edit
283 def test_get_edit
284 @request.session[:user_id] = 2
284 @request.session[:user_id] = 2
285 get :edit, :id => 1
285 get :edit, :id => 1
286 assert_response :success
286 assert_response :success
287 assert_template 'edit'
287 assert_template 'edit'
288 assert_not_nil assigns(:issue)
288 assert_not_nil assigns(:issue)
289 assert_equal Issue.find(1), assigns(:issue)
289 assert_equal Issue.find(1), assigns(:issue)
290 end
290 end
291
291
292 def test_get_edit_with_params
292 def test_get_edit_with_params
293 @request.session[:user_id] = 2
293 @request.session[:user_id] = 2
294 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
294 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
295 assert_response :success
295 assert_response :success
296 assert_template 'edit'
296 assert_template 'edit'
297
297
298 issue = assigns(:issue)
298 issue = assigns(:issue)
299 assert_not_nil issue
299 assert_not_nil issue
300
300
301 assert_equal 5, issue.status_id
301 assert_equal 5, issue.status_id
302 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
302 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
303 :child => { :tag => 'option',
303 :child => { :tag => 'option',
304 :content => 'Closed',
304 :content => 'Closed',
305 :attributes => { :selected => 'selected' } }
305 :attributes => { :selected => 'selected' } }
306
306
307 assert_equal 7, issue.priority_id
307 assert_equal 7, issue.priority_id
308 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
308 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
309 :child => { :tag => 'option',
309 :child => { :tag => 'option',
310 :content => 'Urgent',
310 :content => 'Urgent',
311 :attributes => { :selected => 'selected' } }
311 :attributes => { :selected => 'selected' } }
312 end
312 end
313
313
314 def test_reply_to_issue
314 def test_reply_to_issue
315 @request.session[:user_id] = 2
315 @request.session[:user_id] = 2
316 get :reply, :id => 1
316 get :reply, :id => 1
317 assert_response :success
317 assert_response :success
318 assert_select_rjs :show, "update"
318 assert_select_rjs :show, "update"
319 end
319 end
320
320
321 def test_reply_to_note
321 def test_reply_to_note
322 @request.session[:user_id] = 2
322 @request.session[:user_id] = 2
323 get :reply, :id => 1, :journal_id => 2
323 get :reply, :id => 1, :journal_id => 2
324 assert_response :success
324 assert_response :success
325 assert_select_rjs :show, "update"
325 assert_select_rjs :show, "update"
326 end
326 end
327
327
328 def test_post_edit_without_custom_fields_param
328 def test_post_edit_without_custom_fields_param
329 @request.session[:user_id] = 2
329 @request.session[:user_id] = 2
330 ActionMailer::Base.deliveries.clear
330 ActionMailer::Base.deliveries.clear
331
331
332 issue = Issue.find(1)
332 issue = Issue.find(1)
333 assert_equal '125', issue.custom_value_for(2).value
333 assert_equal '125', issue.custom_value_for(2).value
334 old_subject = issue.subject
334 old_subject = issue.subject
335 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
335 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
336
336
337 assert_difference('Journal.count') do
337 assert_difference('Journal.count') do
338 assert_difference('JournalDetail.count', 2) do
338 assert_difference('JournalDetail.count', 2) do
339 post :edit, :id => 1, :issue => {:subject => new_subject,
339 post :edit, :id => 1, :issue => {:subject => new_subject,
340 :priority_id => '6',
340 :priority_id => '6',
341 :category_id => '1' # no change
341 :category_id => '1' # no change
342 }
342 }
343 end
343 end
344 end
344 end
345 assert_redirected_to 'issues/show/1'
345 assert_redirected_to 'issues/show/1'
346 issue.reload
346 issue.reload
347 assert_equal new_subject, issue.subject
347 assert_equal new_subject, issue.subject
348 # Make sure custom fields were not cleared
348 # Make sure custom fields were not cleared
349 assert_equal '125', issue.custom_value_for(2).value
349 assert_equal '125', issue.custom_value_for(2).value
350
350
351 mail = ActionMailer::Base.deliveries.last
351 mail = ActionMailer::Base.deliveries.last
352 assert_kind_of TMail::Mail, mail
352 assert_kind_of TMail::Mail, mail
353 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
353 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
354 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
354 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
355 end
355 end
356
356
357 def test_post_edit_with_custom_field_change
357 def test_post_edit_with_custom_field_change
358 @request.session[:user_id] = 2
358 @request.session[:user_id] = 2
359 issue = Issue.find(1)
359 issue = Issue.find(1)
360 assert_equal '125', issue.custom_value_for(2).value
360 assert_equal '125', issue.custom_value_for(2).value
361
361
362 assert_difference('Journal.count') do
362 assert_difference('Journal.count') do
363 assert_difference('JournalDetail.count', 3) do
363 assert_difference('JournalDetail.count', 3) do
364 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
364 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
365 :priority_id => '6',
365 :priority_id => '6',
366 :category_id => '1', # no change
366 :category_id => '1', # no change
367 :custom_field_values => { '2' => 'New custom value' }
367 :custom_field_values => { '2' => 'New custom value' }
368 }
368 }
369 end
369 end
370 end
370 end
371 assert_redirected_to 'issues/show/1'
371 assert_redirected_to 'issues/show/1'
372 issue.reload
372 issue.reload
373 assert_equal 'New custom value', issue.custom_value_for(2).value
373 assert_equal 'New custom value', issue.custom_value_for(2).value
374
374
375 mail = ActionMailer::Base.deliveries.last
375 mail = ActionMailer::Base.deliveries.last
376 assert_kind_of TMail::Mail, mail
376 assert_kind_of TMail::Mail, mail
377 assert mail.body.include?("Searchable field changed from 125 to New custom value")
377 assert mail.body.include?("Searchable field changed from 125 to New custom value")
378 end
378 end
379
379
380 def test_post_edit_with_status_and_assignee_change
380 def test_post_edit_with_status_and_assignee_change
381 issue = Issue.find(1)
381 issue = Issue.find(1)
382 assert_equal 1, issue.status_id
382 assert_equal 1, issue.status_id
383 @request.session[:user_id] = 2
383 @request.session[:user_id] = 2
384 assert_difference('TimeEntry.count', 0) do
384 assert_difference('TimeEntry.count', 0) do
385 post :edit,
385 post :edit,
386 :id => 1,
386 :id => 1,
387 :issue => { :status_id => 2, :assigned_to_id => 3 },
387 :issue => { :status_id => 2, :assigned_to_id => 3 },
388 :notes => 'Assigned to dlopper',
388 :notes => 'Assigned to dlopper',
389 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
389 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
390 end
390 end
391 assert_redirected_to 'issues/show/1'
391 assert_redirected_to 'issues/show/1'
392 issue.reload
392 issue.reload
393 assert_equal 2, issue.status_id
393 assert_equal 2, issue.status_id
394 j = issue.journals.find(:first, :order => 'id DESC')
394 j = issue.journals.find(:first, :order => 'id DESC')
395 assert_equal 'Assigned to dlopper', j.notes
395 assert_equal 'Assigned to dlopper', j.notes
396 assert_equal 2, j.details.size
396 assert_equal 2, j.details.size
397
397
398 mail = ActionMailer::Base.deliveries.last
398 mail = ActionMailer::Base.deliveries.last
399 assert mail.body.include?("Status changed from New to Assigned")
399 assert mail.body.include?("Status changed from New to Assigned")
400 end
400 end
401
401
402 def test_post_edit_with_note_only
402 def test_post_edit_with_note_only
403 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
403 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
404 # anonymous user
404 # anonymous user
405 post :edit,
405 post :edit,
406 :id => 1,
406 :id => 1,
407 :notes => notes
407 :notes => notes
408 assert_redirected_to 'issues/show/1'
408 assert_redirected_to 'issues/show/1'
409 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
409 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
410 assert_equal notes, j.notes
410 assert_equal notes, j.notes
411 assert_equal 0, j.details.size
411 assert_equal 0, j.details.size
412 assert_equal User.anonymous, j.user
412 assert_equal User.anonymous, j.user
413
413
414 mail = ActionMailer::Base.deliveries.last
414 mail = ActionMailer::Base.deliveries.last
415 assert mail.body.include?(notes)
415 assert mail.body.include?(notes)
416 end
416 end
417
417
418 def test_post_edit_with_note_and_spent_time
418 def test_post_edit_with_note_and_spent_time
419 @request.session[:user_id] = 2
419 @request.session[:user_id] = 2
420 spent_hours_before = Issue.find(1).spent_hours
420 spent_hours_before = Issue.find(1).spent_hours
421 assert_difference('TimeEntry.count') do
421 assert_difference('TimeEntry.count') do
422 post :edit,
422 post :edit,
423 :id => 1,
423 :id => 1,
424 :notes => '2.5 hours added',
424 :notes => '2.5 hours added',
425 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
425 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
426 end
426 end
427 assert_redirected_to 'issues/show/1'
427 assert_redirected_to 'issues/show/1'
428
428
429 issue = Issue.find(1)
429 issue = Issue.find(1)
430
430
431 j = issue.journals.find(:first, :order => 'id DESC')
431 j = issue.journals.find(:first, :order => 'id DESC')
432 assert_equal '2.5 hours added', j.notes
432 assert_equal '2.5 hours added', j.notes
433 assert_equal 0, j.details.size
433 assert_equal 0, j.details.size
434
434
435 t = issue.time_entries.find(:first, :order => 'id DESC')
435 t = issue.time_entries.find(:first, :order => 'id DESC')
436 assert_not_nil t
436 assert_not_nil t
437 assert_equal 2.5, t.hours
437 assert_equal 2.5, t.hours
438 assert_equal spent_hours_before + 2.5, issue.spent_hours
438 assert_equal spent_hours_before + 2.5, issue.spent_hours
439 end
439 end
440
440
441 def test_post_edit_with_attachment_only
441 def test_post_edit_with_attachment_only
442 set_tmp_attachments_directory
442 set_tmp_attachments_directory
443
443
444 # anonymous user
444 # anonymous user
445 post :edit,
445 post :edit,
446 :id => 1,
446 :id => 1,
447 :notes => '',
447 :notes => '',
448 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
448 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
449 assert_redirected_to 'issues/show/1'
449 assert_redirected_to 'issues/show/1'
450 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
450 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
451 assert j.notes.blank?
451 assert j.notes.blank?
452 assert_equal 1, j.details.size
452 assert_equal 1, j.details.size
453 assert_equal 'testfile.txt', j.details.first.value
453 assert_equal 'testfile.txt', j.details.first.value
454 assert_equal User.anonymous, j.user
454 assert_equal User.anonymous, j.user
455
455
456 mail = ActionMailer::Base.deliveries.last
456 mail = ActionMailer::Base.deliveries.last
457 assert mail.body.include?('testfile.txt')
457 assert mail.body.include?('testfile.txt')
458 end
458 end
459
459
460 def test_post_edit_with_no_change
460 def test_post_edit_with_no_change
461 issue = Issue.find(1)
461 issue = Issue.find(1)
462 issue.journals.clear
462 issue.journals.clear
463 ActionMailer::Base.deliveries.clear
463 ActionMailer::Base.deliveries.clear
464
464
465 post :edit,
465 post :edit,
466 :id => 1,
466 :id => 1,
467 :notes => ''
467 :notes => ''
468 assert_redirected_to 'issues/show/1'
468 assert_redirected_to 'issues/show/1'
469
469
470 issue.reload
470 issue.reload
471 assert issue.journals.empty?
471 assert issue.journals.empty?
472 # No email should be sent
472 # No email should be sent
473 assert ActionMailer::Base.deliveries.empty?
473 assert ActionMailer::Base.deliveries.empty?
474 end
474 end
475
475
476 def test_bulk_edit
476 def test_bulk_edit
477 @request.session[:user_id] = 2
477 @request.session[:user_id] = 2
478 # update issues priority
478 # update issues priority
479 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
479 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
480 assert_response 302
480 assert_response 302
481 # check that the issues were updated
481 # check that the issues were updated
482 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
482 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
483 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
483 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
484 end
484 end
485
485
486 def test_bulk_unassign
486 def test_bulk_unassign
487 assert_not_nil Issue.find(2).assigned_to
487 assert_not_nil Issue.find(2).assigned_to
488 @request.session[:user_id] = 2
488 @request.session[:user_id] = 2
489 # unassign issues
489 # unassign issues
490 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
490 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
491 assert_response 302
491 assert_response 302
492 # check that the issues were updated
492 # check that the issues were updated
493 assert_nil Issue.find(2).assigned_to
493 assert_nil Issue.find(2).assigned_to
494 end
494 end
495
495
496 def test_move_one_issue_to_another_project
496 def test_move_one_issue_to_another_project
497 @request.session[:user_id] = 1
497 @request.session[:user_id] = 1
498 post :move, :id => 1, :new_project_id => 2
498 post :move, :id => 1, :new_project_id => 2
499 assert_redirected_to 'projects/ecookbook/issues'
499 assert_redirected_to 'projects/ecookbook/issues'
500 assert_equal 2, Issue.find(1).project_id
500 assert_equal 2, Issue.find(1).project_id
501 end
501 end
502
502
503 def test_bulk_move_to_another_project
503 def test_bulk_move_to_another_project
504 @request.session[:user_id] = 1
504 @request.session[:user_id] = 1
505 post :move, :ids => [1, 2], :new_project_id => 2
505 post :move, :ids => [1, 2], :new_project_id => 2
506 assert_redirected_to 'projects/ecookbook/issues'
506 assert_redirected_to 'projects/ecookbook/issues'
507 # Issues moved to project 2
507 # Issues moved to project 2
508 assert_equal 2, Issue.find(1).project_id
508 assert_equal 2, Issue.find(1).project_id
509 assert_equal 2, Issue.find(2).project_id
509 assert_equal 2, Issue.find(2).project_id
510 # No tracker change
510 # No tracker change
511 assert_equal 1, Issue.find(1).tracker_id
511 assert_equal 1, Issue.find(1).tracker_id
512 assert_equal 2, Issue.find(2).tracker_id
512 assert_equal 2, Issue.find(2).tracker_id
513 end
513 end
514
514
515 def test_bulk_move_to_another_tracker
515 def test_bulk_move_to_another_tracker
516 @request.session[:user_id] = 1
516 @request.session[:user_id] = 1
517 post :move, :ids => [1, 2], :new_tracker_id => 2
517 post :move, :ids => [1, 2], :new_tracker_id => 2
518 assert_redirected_to 'projects/ecookbook/issues'
518 assert_redirected_to 'projects/ecookbook/issues'
519 assert_equal 2, Issue.find(1).tracker_id
519 assert_equal 2, Issue.find(1).tracker_id
520 assert_equal 2, Issue.find(2).tracker_id
520 assert_equal 2, Issue.find(2).tracker_id
521 end
521 end
522
522
523 def test_context_menu_one_issue
523 def test_context_menu_one_issue
524 @request.session[:user_id] = 2
524 @request.session[:user_id] = 2
525 get :context_menu, :ids => [1]
525 get :context_menu, :ids => [1]
526 assert_response :success
526 assert_response :success
527 assert_template 'context_menu'
527 assert_template 'context_menu'
528 assert_tag :tag => 'a', :content => 'Edit',
528 assert_tag :tag => 'a', :content => 'Edit',
529 :attributes => { :href => '/issues/edit/1',
529 :attributes => { :href => '/issues/edit/1',
530 :class => 'icon-edit' }
530 :class => 'icon-edit' }
531 assert_tag :tag => 'a', :content => 'Closed',
531 assert_tag :tag => 'a', :content => 'Closed',
532 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
532 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
533 :class => '' }
533 :class => '' }
534 assert_tag :tag => 'a', :content => 'Immediate',
534 assert_tag :tag => 'a', :content => 'Immediate',
535 :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
535 :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
536 :class => '' }
536 :class => '' }
537 assert_tag :tag => 'a', :content => 'Dave Lopper',
537 assert_tag :tag => 'a', :content => 'Dave Lopper',
538 :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
538 :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
539 :class => '' }
539 :class => '' }
540 assert_tag :tag => 'a', :content => 'Copy',
540 assert_tag :tag => 'a', :content => 'Copy',
541 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
541 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
542 :class => 'icon-copy' }
542 :class => 'icon-copy' }
543 assert_tag :tag => 'a', :content => 'Move',
543 assert_tag :tag => 'a', :content => 'Move',
544 :attributes => { :href => '/issues/move?ids%5B%5D=1',
544 :attributes => { :href => '/issues/move?ids%5B%5D=1',
545 :class => 'icon-move' }
545 :class => 'icon-move' }
546 assert_tag :tag => 'a', :content => 'Delete',
546 assert_tag :tag => 'a', :content => 'Delete',
547 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
547 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
548 :class => 'icon-del' }
548 :class => 'icon-del' }
549 end
549 end
550
550
551 def test_context_menu_one_issue_by_anonymous
551 def test_context_menu_one_issue_by_anonymous
552 get :context_menu, :ids => [1]
552 get :context_menu, :ids => [1]
553 assert_response :success
553 assert_response :success
554 assert_template 'context_menu'
554 assert_template 'context_menu'
555 assert_tag :tag => 'a', :content => 'Delete',
555 assert_tag :tag => 'a', :content => 'Delete',
556 :attributes => { :href => '#',
556 :attributes => { :href => '#',
557 :class => 'icon-del disabled' }
557 :class => 'icon-del disabled' }
558 end
558 end
559
559
560 def test_context_menu_multiple_issues_of_same_project
560 def test_context_menu_multiple_issues_of_same_project
561 @request.session[:user_id] = 2
561 @request.session[:user_id] = 2
562 get :context_menu, :ids => [1, 2]
562 get :context_menu, :ids => [1, 2]
563 assert_response :success
563 assert_response :success
564 assert_template 'context_menu'
564 assert_template 'context_menu'
565 assert_tag :tag => 'a', :content => 'Edit',
565 assert_tag :tag => 'a', :content => 'Edit',
566 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
566 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
567 :class => 'icon-edit' }
567 :class => 'icon-edit' }
568 assert_tag :tag => 'a', :content => 'Move',
568 assert_tag :tag => 'a', :content => 'Move',
569 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
569 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
570 :class => 'icon-move' }
570 :class => 'icon-move' }
571 assert_tag :tag => 'a', :content => 'Delete',
571 assert_tag :tag => 'a', :content => 'Delete',
572 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
572 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
573 :class => 'icon-del' }
573 :class => 'icon-del' }
574 end
574 end
575
575
576 def test_context_menu_multiple_issues_of_different_project
576 def test_context_menu_multiple_issues_of_different_project
577 @request.session[:user_id] = 2
577 @request.session[:user_id] = 2
578 get :context_menu, :ids => [1, 2, 4]
578 get :context_menu, :ids => [1, 2, 4]
579 assert_response :success
579 assert_response :success
580 assert_template 'context_menu'
580 assert_template 'context_menu'
581 assert_tag :tag => 'a', :content => 'Delete',
581 assert_tag :tag => 'a', :content => 'Delete',
582 :attributes => { :href => '#',
582 :attributes => { :href => '#',
583 :class => 'icon-del disabled' }
583 :class => 'icon-del disabled' }
584 end
584 end
585
585
586 def test_destroy_issue_with_no_time_entries
586 def test_destroy_issue_with_no_time_entries
587 assert_nil TimeEntry.find_by_issue_id(2)
587 assert_nil TimeEntry.find_by_issue_id(2)
588 @request.session[:user_id] = 2
588 @request.session[:user_id] = 2
589 post :destroy, :id => 2
589 post :destroy, :id => 2
590 assert_redirected_to 'projects/ecookbook/issues'
590 assert_redirected_to 'projects/ecookbook/issues'
591 assert_nil Issue.find_by_id(2)
591 assert_nil Issue.find_by_id(2)
592 end
592 end
593
593
594 def test_destroy_issues_with_time_entries
594 def test_destroy_issues_with_time_entries
595 @request.session[:user_id] = 2
595 @request.session[:user_id] = 2
596 post :destroy, :ids => [1, 3]
596 post :destroy, :ids => [1, 3]
597 assert_response :success
597 assert_response :success
598 assert_template 'destroy'
598 assert_template 'destroy'
599 assert_not_nil assigns(:hours)
599 assert_not_nil assigns(:hours)
600 assert Issue.find_by_id(1) && Issue.find_by_id(3)
600 assert Issue.find_by_id(1) && Issue.find_by_id(3)
601 end
601 end
602
602
603 def test_destroy_issues_and_destroy_time_entries
603 def test_destroy_issues_and_destroy_time_entries
604 @request.session[:user_id] = 2
604 @request.session[:user_id] = 2
605 post :destroy, :ids => [1, 3], :todo => 'destroy'
605 post :destroy, :ids => [1, 3], :todo => 'destroy'
606 assert_redirected_to 'projects/ecookbook/issues'
606 assert_redirected_to 'projects/ecookbook/issues'
607 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
607 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
608 assert_nil TimeEntry.find_by_id([1, 2])
608 assert_nil TimeEntry.find_by_id([1, 2])
609 end
609 end
610
610
611 def test_destroy_issues_and_assign_time_entries_to_project
611 def test_destroy_issues_and_assign_time_entries_to_project
612 @request.session[:user_id] = 2
612 @request.session[:user_id] = 2
613 post :destroy, :ids => [1, 3], :todo => 'nullify'
613 post :destroy, :ids => [1, 3], :todo => 'nullify'
614 assert_redirected_to 'projects/ecookbook/issues'
614 assert_redirected_to 'projects/ecookbook/issues'
615 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
615 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
616 assert_nil TimeEntry.find(1).issue_id
616 assert_nil TimeEntry.find(1).issue_id
617 assert_nil TimeEntry.find(2).issue_id
617 assert_nil TimeEntry.find(2).issue_id
618 end
618 end
619
619
620 def test_destroy_issues_and_reassign_time_entries_to_another_issue
620 def test_destroy_issues_and_reassign_time_entries_to_another_issue
621 @request.session[:user_id] = 2
621 @request.session[:user_id] = 2
622 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
622 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
623 assert_redirected_to 'projects/ecookbook/issues'
623 assert_redirected_to 'projects/ecookbook/issues'
624 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
624 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
625 assert_equal 2, TimeEntry.find(1).issue_id
625 assert_equal 2, TimeEntry.find(1).issue_id
626 assert_equal 2, TimeEntry.find(2).issue_id
626 assert_equal 2, TimeEntry.find(2).issue_id
627 end
627 end
628
628
629 def test_destroy_attachment
629 def test_destroy_attachment
630 issue = Issue.find(3)
630 issue = Issue.find(3)
631 a = issue.attachments.size
631 a = issue.attachments.size
632 @request.session[:user_id] = 2
632 @request.session[:user_id] = 2
633 post :destroy_attachment, :id => 3, :attachment_id => 1
633 post :destroy_attachment, :id => 3, :attachment_id => 1
634 assert_redirected_to 'issues/show/3'
634 assert_redirected_to 'issues/show/3'
635 assert_nil Attachment.find_by_id(1)
635 assert_nil Attachment.find_by_id(1)
636 issue.reload
636 issue.reload
637 assert_equal((a-1), issue.attachments.size)
637 assert_equal((a-1), issue.attachments.size)
638 j = issue.journals.find(:first, :order => 'created_on DESC')
638 j = issue.journals.find(:first, :order => 'created_on DESC')
639 assert_equal 'attachment', j.details.first.property
639 assert_equal 'attachment', j.details.first.property
640 end
640 end
641 end
641 end
@@ -1,82 +1,85
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, :dependent => :delete_all, :as => :customized
30 has_many :custom_values, :as => :customized,
31 :include => :custom_field,
32 :order => "#{CustomField.table_name}.position",
33 :dependent => :delete_all
31 before_validation_on_create { |customized| customized.custom_field_values }
34 before_validation_on_create { |customized| customized.custom_field_values }
32 # Trigger validation only if custom values were changed
35 # Trigger validation only if custom values were changed
33 validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
36 validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
34 send :include, Redmine::Acts::Customizable::InstanceMethods
37 send :include, Redmine::Acts::Customizable::InstanceMethods
35 # Save custom values when saving the customized object
38 # Save custom values when saving the customized object
36 after_save :save_custom_field_values
39 after_save :save_custom_field_values
37 end
40 end
38 end
41 end
39
42
40 module InstanceMethods
43 module InstanceMethods
41 def self.included(base)
44 def self.included(base)
42 base.extend ClassMethods
45 base.extend ClassMethods
43 end
46 end
44
47
45 def available_custom_fields
48 def available_custom_fields
46 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
49 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
47 :order => 'position')
50 :order => 'position')
48 end
51 end
49
52
50 def custom_field_values=(values)
53 def custom_field_values=(values)
51 @custom_field_values_changed = true
54 @custom_field_values_changed = true
52 values = values.stringify_keys
55 values = values.stringify_keys
53 custom_field_values.each do |custom_value|
56 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)
57 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)
58 end if values.is_a?(Hash)
56 end
59 end
57
60
58 def custom_field_values
61 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) }
62 @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
63 end
61
64
62 def custom_field_values_changed?
65 def custom_field_values_changed?
63 @custom_field_values_changed == true
66 @custom_field_values_changed == true
64 end
67 end
65
68
66 def custom_value_for(c)
69 def custom_value_for(c)
67 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
70 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
68 custom_values.detect {|v| v.custom_field_id == field_id }
71 custom_values.detect {|v| v.custom_field_id == field_id }
69 end
72 end
70
73
71 def save_custom_field_values
74 def save_custom_field_values
72 custom_field_values.each(&:save)
75 custom_field_values.each(&:save)
73 @custom_field_values_changed = false
76 @custom_field_values_changed = false
74 @custom_field_values = nil
77 @custom_field_values = nil
75 end
78 end
76
79
77 module ClassMethods
80 module ClassMethods
78 end
81 end
79 end
82 end
80 end
83 end
81 end
84 end
82 end
85 end
General Comments 0
You need to be logged in to leave comments. Login now