##// END OF EJS Templates
Converted User#mail_notification from a boolean to a string....
Eric Davis -
r4102:0316af7f6bfa
parent child
Show More
@@ -0,0 +1,9
1 class ChangeUsersMailNotificationToString < ActiveRecord::Migration
2 def self.up
3 change_column :users, :mail_notification, :string, :default => '', :null => false
4 end
5
6 def self.down
7 change_column :users, :mail_notification, :boolean, :default => true, :null => false
8 end
9 end
@@ -0,0 +1,11
1 # Patch the data from a boolean change.
2 class UpdateMailNotificationValues < ActiveRecord::Migration
3 def self.up
4 User.update_all("mail_notification = 'all'", "mail_notification = '1'")
5 User.update_all("mail_notification = 'only_my_events'", "mail_notification = '0'")
6 end
7
8 def self.down
9 # No-op
10 end
11 end
@@ -1,183 +1,185
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MyController < ApplicationController
19 19 before_filter :require_login
20 20
21 21 helper :issues
22 22 helper :custom_fields
23 23
24 24 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
25 25 'issuesreportedbyme' => :label_reported_issues,
26 26 'issueswatched' => :label_watched_issues,
27 27 'news' => :label_news_latest,
28 28 'calendar' => :label_calendar,
29 29 'documents' => :label_document_plural,
30 30 'timelog' => :label_spent_time
31 31 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
32 32
33 33 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
34 34 'right' => ['issuesreportedbyme']
35 35 }.freeze
36 36
37 37 verify :xhr => true,
38 38 :only => [:add_block, :remove_block, :order_blocks]
39 39
40 40 def index
41 41 page
42 42 render :action => 'page'
43 43 end
44 44
45 45 # Show user's page
46 46 def page
47 47 @user = User.current
48 48 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
49 49 end
50 50
51 51 # Edit user's account
52 52 def account
53 53 @user = User.current
54 54 @pref = @user.pref
55 55 if request.post?
56 56 @user.attributes = params[:user]
57 @user.mail_notification = (params[:notification_option] == 'all')
57 @user.mail_notification = params[:notification_option] || 'only_my_events'
58 58 @user.pref.attributes = params[:pref]
59 59 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
60 60 if @user.save
61 61 @user.pref.save
62 62 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
63 63 set_language_if_valid @user.language
64 64 flash[:notice] = l(:notice_account_updated)
65 65 redirect_to :action => 'account'
66 66 return
67 67 end
68 68 end
69 @notification_options = [[l(:label_user_mail_option_all), 'all'],
70 [l(:label_user_mail_option_none), 'none']]
69 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
71 70 # Only users that belong to more than 1 project can select projects for which they are notified
72 # Note that @user.membership.size would fail since AR ignores :include association option when doing a count
73 @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
74 @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
71 # Note that @user.membership.size would fail since AR ignores
72 # :include association option when doing a count
73 if @user.memberships.length < 1
74 @notification_options.delete_if {|option| option.first == :selected}
75 end
76 @notification_option = @user.mail_notification #? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
75 77 end
76 78
77 79 # Manage user's password
78 80 def password
79 81 @user = User.current
80 82 unless @user.change_password_allowed?
81 83 flash[:error] = l(:notice_can_t_change_password)
82 84 redirect_to :action => 'account'
83 85 return
84 86 end
85 87 if request.post?
86 88 if @user.check_password?(params[:password])
87 89 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
88 90 if @user.save
89 91 flash[:notice] = l(:notice_account_password_updated)
90 92 redirect_to :action => 'account'
91 93 end
92 94 else
93 95 flash[:error] = l(:notice_account_wrong_password)
94 96 end
95 97 end
96 98 end
97 99
98 100 # Create a new feeds key
99 101 def reset_rss_key
100 102 if request.post?
101 103 if User.current.rss_token
102 104 User.current.rss_token.destroy
103 105 User.current.reload
104 106 end
105 107 User.current.rss_key
106 108 flash[:notice] = l(:notice_feeds_access_key_reseted)
107 109 end
108 110 redirect_to :action => 'account'
109 111 end
110 112
111 113 # Create a new API key
112 114 def reset_api_key
113 115 if request.post?
114 116 if User.current.api_token
115 117 User.current.api_token.destroy
116 118 User.current.reload
117 119 end
118 120 User.current.api_key
119 121 flash[:notice] = l(:notice_api_access_key_reseted)
120 122 end
121 123 redirect_to :action => 'account'
122 124 end
123 125
124 126 # User's page layout configuration
125 127 def page_layout
126 128 @user = User.current
127 129 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
128 130 @block_options = []
129 131 BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
130 132 end
131 133
132 134 # Add a block to user's page
133 135 # The block is added on top of the page
134 136 # params[:block] : id of the block to add
135 137 def add_block
136 138 block = params[:block].to_s.underscore
137 139 (render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
138 140 @user = User.current
139 141 layout = @user.pref[:my_page_layout] || {}
140 142 # remove if already present in a group
141 143 %w(top left right).each {|f| (layout[f] ||= []).delete block }
142 144 # add it on top
143 145 layout['top'].unshift block
144 146 @user.pref[:my_page_layout] = layout
145 147 @user.pref.save
146 148 render :partial => "block", :locals => {:user => @user, :block_name => block}
147 149 end
148 150
149 151 # Remove a block to user's page
150 152 # params[:block] : id of the block to remove
151 153 def remove_block
152 154 block = params[:block].to_s.underscore
153 155 @user = User.current
154 156 # remove block in all groups
155 157 layout = @user.pref[:my_page_layout] || {}
156 158 %w(top left right).each {|f| (layout[f] ||= []).delete block }
157 159 @user.pref[:my_page_layout] = layout
158 160 @user.pref.save
159 161 render :nothing => true
160 162 end
161 163
162 164 # Change blocks order on user's page
163 165 # params[:group] : group to order (top, left or right)
164 166 # params[:list-(top|left|right)] : array of block ids of the group
165 167 def order_blocks
166 168 group = params[:group]
167 169 @user = User.current
168 170 if group.is_a?(String)
169 171 group_items = (params["list-#{group}"] || []).collect(&:underscore)
170 172 if group_items and group_items.is_a? Array
171 173 layout = @user.pref[:my_page_layout] || {}
172 174 # remove group blocks if they are presents in other groups
173 175 %w(top left right).each {|f|
174 176 layout[f] = (layout[f] || []) - group_items
175 177 }
176 178 layout[group] = group_items
177 179 @user.pref[:my_page_layout] = layout
178 180 @user.pref.save
179 181 end
180 182 end
181 183 render :nothing => true
182 184 end
183 185 end
@@ -1,774 +1,774
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :memberships, :class_name => 'Member'
27 27 has_many :member_principals, :class_name => 'Member',
28 28 :include => :principal,
29 29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 30 has_many :users, :through => :members
31 31 has_many :principals, :through => :member_principals, :source => :principal
32 32
33 33 has_many :enabled_modules, :dependent => :delete_all
34 34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 36 has_many :issue_changes, :through => :issues, :source => :journals
37 37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 38 has_many :time_entries, :dependent => :delete_all
39 39 has_many :queries, :dependent => :delete_all
40 40 has_many :documents, :dependent => :destroy
41 41 has_many :news, :dependent => :delete_all, :include => :author
42 42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 44 has_one :repository, :dependent => :destroy
45 45 has_many :changesets, :through => :repository
46 46 has_one :wiki, :dependent => :destroy
47 47 # Custom field for the project issues
48 48 has_and_belongs_to_many :issue_custom_fields,
49 49 :class_name => 'IssueCustomField',
50 50 :order => "#{CustomField.table_name}.position",
51 51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 52 :association_foreign_key => 'custom_field_id'
53 53
54 54 acts_as_nested_set :order => 'name'
55 55 acts_as_attachable :view_permission => :view_files,
56 56 :delete_permission => :manage_files
57 57
58 58 acts_as_customizable
59 59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 62 :author => nil
63 63
64 64 attr_protected :status, :enabled_module_names
65 65
66 66 validates_presence_of :name, :identifier
67 67 validates_uniqueness_of :name, :identifier
68 68 validates_associated :repository, :wiki
69 69 validates_length_of :name, :maximum => 30
70 70 validates_length_of :homepage, :maximum => 255
71 71 validates_length_of :identifier, :in => 1..20
72 72 # donwcase letters, digits, dashes but not digits only
73 73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 74 # reserved words
75 75 validates_exclusion_of :identifier, :in => %w( new )
76 76
77 77 before_destroy :delete_all_members, :destroy_children
78 78
79 79 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
80 80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 81 named_scope :all_public, { :conditions => { :is_public => true } }
82 82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83 83
84 84 def identifier=(identifier)
85 85 super unless identifier_frozen?
86 86 end
87 87
88 88 def identifier_frozen?
89 89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 90 end
91 91
92 92 # returns latest created projects
93 93 # non public projects will be returned only if user is a member of those
94 94 def self.latest(user=nil, count=5)
95 95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 96 end
97 97
98 98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 99 #
100 100 # Examples:
101 101 # Projects.visible_by(admin) => "projects.status = 1"
102 102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 103 def self.visible_by(user=nil)
104 104 user ||= User.current
105 105 if user && user.admin?
106 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 107 elsif user && user.memberships.any?
108 108 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(',')}))"
109 109 else
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 111 end
112 112 end
113 113
114 114 def self.allowed_to_condition(user, permission, options={})
115 115 statements = []
116 116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 117 if perm = Redmine::AccessControl.permission(permission)
118 118 unless perm.project_module.nil?
119 119 # If the permission belongs to a project module, make sure the module is enabled
120 120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 121 end
122 122 end
123 123 if options[:project]
124 124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 127 end
128 128 if user.admin?
129 129 # no restriction
130 130 else
131 131 statements << "1=0"
132 132 if user.logged?
133 133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 135 end
136 136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 138 else
139 139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 140 # anonymous user allowed on public project
141 141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 142 end
143 143 end
144 144 end
145 145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 146 end
147 147
148 148 # Returns the Systemwide and project specific activities
149 149 def activities(include_inactive=false)
150 150 if include_inactive
151 151 return all_activities
152 152 else
153 153 return active_activities
154 154 end
155 155 end
156 156
157 157 # Will create a new Project specific Activity or update an existing one
158 158 #
159 159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 160 # does not successfully save.
161 161 def update_or_create_time_entry_activity(id, activity_hash)
162 162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 163 self.create_time_entry_activity_if_needed(activity_hash)
164 164 else
165 165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 166 activity.update_attributes(activity_hash) if activity
167 167 end
168 168 end
169 169
170 170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 171 #
172 172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 173 # does not successfully save.
174 174 def create_time_entry_activity_if_needed(activity)
175 175 if activity['parent_id']
176 176
177 177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 178 activity['name'] = parent_activity.name
179 179 activity['position'] = parent_activity.position
180 180
181 181 if Enumeration.overridding_change?(activity, parent_activity)
182 182 project_activity = self.time_entry_activities.create(activity)
183 183
184 184 if project_activity.new_record?
185 185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 186 else
187 187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 188 end
189 189 end
190 190 end
191 191 end
192 192
193 193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 194 #
195 195 # Examples:
196 196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 197 # project.project_condition(false) => "projects.id = 1"
198 198 def project_condition(with_subprojects)
199 199 cond = "#{Project.table_name}.id = #{id}"
200 200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 201 cond
202 202 end
203 203
204 204 def self.find(*args)
205 205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 206 project = find_by_identifier(*args)
207 207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 208 project
209 209 else
210 210 super
211 211 end
212 212 end
213 213
214 214 def to_param
215 215 # id is used for projects with a numeric identifier (compatibility)
216 216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 217 end
218 218
219 219 def active?
220 220 self.status == STATUS_ACTIVE
221 221 end
222 222
223 223 # Archives the project and its descendants
224 224 def archive
225 225 # Check that there is no issue of a non descendant project that is assigned
226 226 # to one of the project or descendant versions
227 227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 228 if v_ids.any? && Issue.find(:first, :include => :project,
229 229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 231 return false
232 232 end
233 233 Project.transaction do
234 234 archive!
235 235 end
236 236 true
237 237 end
238 238
239 239 # Unarchives the project
240 240 # All its ancestors must be active
241 241 def unarchive
242 242 return false if ancestors.detect {|a| !a.active?}
243 243 update_attribute :status, STATUS_ACTIVE
244 244 end
245 245
246 246 # Returns an array of projects the project can be moved to
247 247 # by the current user
248 248 def allowed_parents
249 249 return @allowed_parents if @allowed_parents
250 250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 251 @allowed_parents = @allowed_parents - self_and_descendants
252 252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 253 @allowed_parents << nil
254 254 end
255 255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 256 @allowed_parents << parent
257 257 end
258 258 @allowed_parents
259 259 end
260 260
261 261 # Sets the parent of the project with authorization check
262 262 def set_allowed_parent!(p)
263 263 unless p.nil? || p.is_a?(Project)
264 264 if p.to_s.blank?
265 265 p = nil
266 266 else
267 267 p = Project.find_by_id(p)
268 268 return false unless p
269 269 end
270 270 end
271 271 if p.nil?
272 272 if !new_record? && allowed_parents.empty?
273 273 return false
274 274 end
275 275 elsif !allowed_parents.include?(p)
276 276 return false
277 277 end
278 278 set_parent!(p)
279 279 end
280 280
281 281 # Sets the parent of the project
282 282 # Argument can be either a Project, a String, a Fixnum or nil
283 283 def set_parent!(p)
284 284 unless p.nil? || p.is_a?(Project)
285 285 if p.to_s.blank?
286 286 p = nil
287 287 else
288 288 p = Project.find_by_id(p)
289 289 return false unless p
290 290 end
291 291 end
292 292 if p == parent && !p.nil?
293 293 # Nothing to do
294 294 true
295 295 elsif p.nil? || (p.active? && move_possible?(p))
296 296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 297 sibs = (p.nil? ? self.class.roots : p.children)
298 298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 299 if to_be_inserted_before
300 300 move_to_left_of(to_be_inserted_before)
301 301 elsif p.nil?
302 302 if sibs.empty?
303 303 # move_to_root adds the project in first (ie. left) position
304 304 move_to_root
305 305 else
306 306 move_to_right_of(sibs.last) unless self == sibs.last
307 307 end
308 308 else
309 309 # move_to_child_of adds the project in last (ie.right) position
310 310 move_to_child_of(p)
311 311 end
312 312 Issue.update_versions_from_hierarchy_change(self)
313 313 true
314 314 else
315 315 # Can not move to the given target
316 316 false
317 317 end
318 318 end
319 319
320 320 # Returns an array of the trackers used by the project and its active sub projects
321 321 def rolled_up_trackers
322 322 @rolled_up_trackers ||=
323 323 Tracker.find(:all, :include => :projects,
324 324 :select => "DISTINCT #{Tracker.table_name}.*",
325 325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 326 :order => "#{Tracker.table_name}.position")
327 327 end
328 328
329 329 # Closes open and locked project versions that are completed
330 330 def close_completed_versions
331 331 Version.transaction do
332 332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 333 if version.completed?
334 334 version.update_attribute(:status, 'closed')
335 335 end
336 336 end
337 337 end
338 338 end
339 339
340 340 # Returns a scope of the Versions on subprojects
341 341 def rolled_up_versions
342 342 @rolled_up_versions ||=
343 343 Version.scoped(:include => :project,
344 344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 345 end
346 346
347 347 # Returns a scope of the Versions used by the project
348 348 def shared_versions
349 349 @shared_versions ||=
350 350 Version.scoped(:include => :project,
351 351 :conditions => "#{Project.table_name}.id = #{id}" +
352 352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
353 353 " #{Version.table_name}.sharing = 'system'" +
354 354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
355 355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
356 356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
357 357 "))")
358 358 end
359 359
360 360 # Returns a hash of project users grouped by role
361 361 def users_by_role
362 362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
363 363 m.roles.each do |r|
364 364 h[r] ||= []
365 365 h[r] << m.user
366 366 end
367 367 h
368 368 end
369 369 end
370 370
371 371 # Deletes all project's members
372 372 def delete_all_members
373 373 me, mr = Member.table_name, MemberRole.table_name
374 374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
375 375 Member.delete_all(['project_id = ?', id])
376 376 end
377 377
378 378 # Users issues can be assigned to
379 379 def assignable_users
380 380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
381 381 end
382 382
383 383 # Returns the mail adresses of users that should be always notified on project events
384 384 def recipients
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
385 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user.mail}
386 386 end
387 387
388 388 # Returns the users that should be notified on project events
389 389 def notified_users
390 390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
391 391 end
392 392
393 393 # Returns an array of all custom fields enabled for project issues
394 394 # (explictly associated custom fields and custom fields enabled for all projects)
395 395 def all_issue_custom_fields
396 396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
397 397 end
398 398
399 399 def project
400 400 self
401 401 end
402 402
403 403 def <=>(project)
404 404 name.downcase <=> project.name.downcase
405 405 end
406 406
407 407 def to_s
408 408 name
409 409 end
410 410
411 411 # Returns a short description of the projects (first lines)
412 412 def short_description(length = 255)
413 413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
414 414 end
415 415
416 416 def css_classes
417 417 s = 'project'
418 418 s << ' root' if root?
419 419 s << ' child' if child?
420 420 s << (leaf? ? ' leaf' : ' parent')
421 421 s
422 422 end
423 423
424 424 # The earliest start date of a project, based on it's issues and versions
425 425 def start_date
426 426 if module_enabled?(:issue_tracking)
427 427 [
428 428 issues.minimum('start_date'),
429 429 shared_versions.collect(&:effective_date),
430 430 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
431 431 ].flatten.compact.min
432 432 end
433 433 end
434 434
435 435 # The latest due date of an issue or version
436 436 def due_date
437 437 if module_enabled?(:issue_tracking)
438 438 [
439 439 issues.maximum('due_date'),
440 440 shared_versions.collect(&:effective_date),
441 441 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
442 442 ].flatten.compact.max
443 443 end
444 444 end
445 445
446 446 def overdue?
447 447 active? && !due_date.nil? && (due_date < Date.today)
448 448 end
449 449
450 450 # Returns the percent completed for this project, based on the
451 451 # progress on it's versions.
452 452 def completed_percent(options={:include_subprojects => false})
453 453 if options.delete(:include_subprojects)
454 454 total = self_and_descendants.collect(&:completed_percent).sum
455 455
456 456 total / self_and_descendants.count
457 457 else
458 458 if versions.count > 0
459 459 total = versions.collect(&:completed_pourcent).sum
460 460
461 461 total / versions.count
462 462 else
463 463 100
464 464 end
465 465 end
466 466 end
467 467
468 468 # Return true if this project is allowed to do the specified action.
469 469 # action can be:
470 470 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
471 471 # * a permission Symbol (eg. :edit_project)
472 472 def allows_to?(action)
473 473 if action.is_a? Hash
474 474 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
475 475 else
476 476 allowed_permissions.include? action
477 477 end
478 478 end
479 479
480 480 def module_enabled?(module_name)
481 481 module_name = module_name.to_s
482 482 enabled_modules.detect {|m| m.name == module_name}
483 483 end
484 484
485 485 def enabled_module_names=(module_names)
486 486 if module_names && module_names.is_a?(Array)
487 487 module_names = module_names.collect(&:to_s)
488 488 # remove disabled modules
489 489 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
490 490 # add new modules
491 491 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
492 492 else
493 493 enabled_modules.clear
494 494 end
495 495 end
496 496
497 497 # Returns an array of projects that are in this project's hierarchy
498 498 #
499 499 # Example: parents, children, siblings
500 500 def hierarchy
501 501 parents = project.self_and_ancestors || []
502 502 descendants = project.descendants || []
503 503 project_hierarchy = parents | descendants # Set union
504 504 end
505 505
506 506 # Returns an auto-generated project identifier based on the last identifier used
507 507 def self.next_identifier
508 508 p = Project.find(:first, :order => 'created_on DESC')
509 509 p.nil? ? nil : p.identifier.to_s.succ
510 510 end
511 511
512 512 # Copies and saves the Project instance based on the +project+.
513 513 # Duplicates the source project's:
514 514 # * Wiki
515 515 # * Versions
516 516 # * Categories
517 517 # * Issues
518 518 # * Members
519 519 # * Queries
520 520 #
521 521 # Accepts an +options+ argument to specify what to copy
522 522 #
523 523 # Examples:
524 524 # project.copy(1) # => copies everything
525 525 # project.copy(1, :only => 'members') # => copies members only
526 526 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
527 527 def copy(project, options={})
528 528 project = project.is_a?(Project) ? project : Project.find(project)
529 529
530 530 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
531 531 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
532 532
533 533 Project.transaction do
534 534 if save
535 535 reload
536 536 to_be_copied.each do |name|
537 537 send "copy_#{name}", project
538 538 end
539 539 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
540 540 save
541 541 end
542 542 end
543 543 end
544 544
545 545
546 546 # Copies +project+ and returns the new instance. This will not save
547 547 # the copy
548 548 def self.copy_from(project)
549 549 begin
550 550 project = project.is_a?(Project) ? project : Project.find(project)
551 551 if project
552 552 # clear unique attributes
553 553 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
554 554 copy = Project.new(attributes)
555 555 copy.enabled_modules = project.enabled_modules
556 556 copy.trackers = project.trackers
557 557 copy.custom_values = project.custom_values.collect {|v| v.clone}
558 558 copy.issue_custom_fields = project.issue_custom_fields
559 559 return copy
560 560 else
561 561 return nil
562 562 end
563 563 rescue ActiveRecord::RecordNotFound
564 564 return nil
565 565 end
566 566 end
567 567
568 568 private
569 569
570 570 # Destroys children before destroying self
571 571 def destroy_children
572 572 children.each do |child|
573 573 child.destroy
574 574 end
575 575 end
576 576
577 577 # Copies wiki from +project+
578 578 def copy_wiki(project)
579 579 # Check that the source project has a wiki first
580 580 unless project.wiki.nil?
581 581 self.wiki ||= Wiki.new
582 582 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
583 583 wiki_pages_map = {}
584 584 project.wiki.pages.each do |page|
585 585 # Skip pages without content
586 586 next if page.content.nil?
587 587 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
588 588 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
589 589 new_wiki_page.content = new_wiki_content
590 590 wiki.pages << new_wiki_page
591 591 wiki_pages_map[page.id] = new_wiki_page
592 592 end
593 593 wiki.save
594 594 # Reproduce page hierarchy
595 595 project.wiki.pages.each do |page|
596 596 if page.parent_id && wiki_pages_map[page.id]
597 597 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
598 598 wiki_pages_map[page.id].save
599 599 end
600 600 end
601 601 end
602 602 end
603 603
604 604 # Copies versions from +project+
605 605 def copy_versions(project)
606 606 project.versions.each do |version|
607 607 new_version = Version.new
608 608 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
609 609 self.versions << new_version
610 610 end
611 611 end
612 612
613 613 # Copies issue categories from +project+
614 614 def copy_issue_categories(project)
615 615 project.issue_categories.each do |issue_category|
616 616 new_issue_category = IssueCategory.new
617 617 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
618 618 self.issue_categories << new_issue_category
619 619 end
620 620 end
621 621
622 622 # Copies issues from +project+
623 623 def copy_issues(project)
624 624 # Stores the source issue id as a key and the copied issues as the
625 625 # value. Used to map the two togeather for issue relations.
626 626 issues_map = {}
627 627
628 628 # Get issues sorted by root_id, lft so that parent issues
629 629 # get copied before their children
630 630 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
631 631 new_issue = Issue.new
632 632 new_issue.copy_from(issue)
633 633 new_issue.project = self
634 634 # Reassign fixed_versions by name, since names are unique per
635 635 # project and the versions for self are not yet saved
636 636 if issue.fixed_version
637 637 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
638 638 end
639 639 # Reassign the category by name, since names are unique per
640 640 # project and the categories for self are not yet saved
641 641 if issue.category
642 642 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
643 643 end
644 644 # Parent issue
645 645 if issue.parent_id
646 646 if copied_parent = issues_map[issue.parent_id]
647 647 new_issue.parent_issue_id = copied_parent.id
648 648 end
649 649 end
650 650
651 651 self.issues << new_issue
652 652 issues_map[issue.id] = new_issue
653 653 end
654 654
655 655 # Relations after in case issues related each other
656 656 project.issues.each do |issue|
657 657 new_issue = issues_map[issue.id]
658 658
659 659 # Relations
660 660 issue.relations_from.each do |source_relation|
661 661 new_issue_relation = IssueRelation.new
662 662 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
663 663 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
664 664 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
665 665 new_issue_relation.issue_to = source_relation.issue_to
666 666 end
667 667 new_issue.relations_from << new_issue_relation
668 668 end
669 669
670 670 issue.relations_to.each do |source_relation|
671 671 new_issue_relation = IssueRelation.new
672 672 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
673 673 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
674 674 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
675 675 new_issue_relation.issue_from = source_relation.issue_from
676 676 end
677 677 new_issue.relations_to << new_issue_relation
678 678 end
679 679 end
680 680 end
681 681
682 682 # Copies members from +project+
683 683 def copy_members(project)
684 684 project.memberships.each do |member|
685 685 new_member = Member.new
686 686 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
687 687 # only copy non inherited roles
688 688 # inherited roles will be added when copying the group membership
689 689 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
690 690 next if role_ids.empty?
691 691 new_member.role_ids = role_ids
692 692 new_member.project = self
693 693 self.members << new_member
694 694 end
695 695 end
696 696
697 697 # Copies queries from +project+
698 698 def copy_queries(project)
699 699 project.queries.each do |query|
700 700 new_query = Query.new
701 701 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
702 702 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
703 703 new_query.project = self
704 704 self.queries << new_query
705 705 end
706 706 end
707 707
708 708 # Copies boards from +project+
709 709 def copy_boards(project)
710 710 project.boards.each do |board|
711 711 new_board = Board.new
712 712 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
713 713 new_board.project = self
714 714 self.boards << new_board
715 715 end
716 716 end
717 717
718 718 def allowed_permissions
719 719 @allowed_permissions ||= begin
720 720 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
721 721 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
722 722 end
723 723 end
724 724
725 725 def allowed_actions
726 726 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
727 727 end
728 728
729 729 # Returns all the active Systemwide and project specific activities
730 730 def active_activities
731 731 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
732 732
733 733 if overridden_activity_ids.empty?
734 734 return TimeEntryActivity.shared.active
735 735 else
736 736 return system_activities_and_project_overrides
737 737 end
738 738 end
739 739
740 740 # Returns all the Systemwide and project specific activities
741 741 # (inactive and active)
742 742 def all_activities
743 743 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
744 744
745 745 if overridden_activity_ids.empty?
746 746 return TimeEntryActivity.shared
747 747 else
748 748 return system_activities_and_project_overrides(true)
749 749 end
750 750 end
751 751
752 752 # Returns the systemwide active activities merged with the project specific overrides
753 753 def system_activities_and_project_overrides(include_inactive=false)
754 754 if include_inactive
755 755 return TimeEntryActivity.shared.
756 756 find(:all,
757 757 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
758 758 self.time_entry_activities
759 759 else
760 760 return TimeEntryActivity.shared.active.
761 761 find(:all,
762 762 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
763 763 self.time_entry_activities.active
764 764 end
765 765 end
766 766
767 767 # Archives subprojects recursively
768 768 def archive!
769 769 children.each do |subproject|
770 770 subproject.send :archive!
771 771 end
772 772 update_attribute :status, STATUS_ARCHIVED
773 773 end
774 774 end
@@ -1,416 +1,425
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 MAIL_NOTIFICATION_OPTIONS = [
37 [:all, :label_user_mail_option_all],
38 [:selected, :label_user_mail_option_selected],
39 [:none, :label_user_mail_option_none],
40 [:only_my_events, :label_user_mail_option_only_my_events],
41 [:only_assigned, :label_user_mail_option_only_assigned],
42 [:only_owner, :label_user_mail_option_only_owner]
43 ]
44
36 45 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 46 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 47 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 48 has_many :changesets, :dependent => :nullify
40 49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 50 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 51 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
43 52 belongs_to :auth_source
44 53
45 54 # Active non-anonymous users scope
46 55 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 56
48 57 acts_as_customizable
49 58
50 59 attr_accessor :password, :password_confirmation
51 60 attr_accessor :last_before_login_on
52 61 # Prevents unauthorized assignments
53 62 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54 63
55 64 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 65 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
57 66 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 67 # Login must contain lettres, numbers, underscores only
59 68 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 69 validates_length_of :login, :maximum => 30
61 70 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 71 validates_length_of :firstname, :lastname, :maximum => 30
63 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 74 validates_confirmation_of :password, :allow_nil => true
66 75
67 76 def before_create
68 self.mail_notification = false
77 self.mail_notification = 'only_my_events'
69 78 true
70 79 end
71 80
72 81 def before_save
73 82 # update hashed_password if password was set
74 83 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
75 84 end
76 85
77 86 def reload(*args)
78 87 @name = nil
79 88 super
80 89 end
81 90
82 91 def mail=(arg)
83 92 write_attribute(:mail, arg.to_s.strip)
84 93 end
85 94
86 95 def identity_url=(url)
87 96 if url.blank?
88 97 write_attribute(:identity_url, '')
89 98 else
90 99 begin
91 100 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
92 101 rescue OpenIdAuthentication::InvalidOpenId
93 102 # Invlaid url, don't save
94 103 end
95 104 end
96 105 self.read_attribute(:identity_url)
97 106 end
98 107
99 108 # Returns the user that matches provided login and password, or nil
100 109 def self.try_to_login(login, password)
101 110 # Make sure no one can sign in with an empty password
102 111 return nil if password.to_s.empty?
103 112 user = find_by_login(login)
104 113 if user
105 114 # user is already in local database
106 115 return nil if !user.active?
107 116 if user.auth_source
108 117 # user has an external authentication method
109 118 return nil unless user.auth_source.authenticate(login, password)
110 119 else
111 120 # authentication with local password
112 121 return nil unless User.hash_password(password) == user.hashed_password
113 122 end
114 123 else
115 124 # user is not yet registered, try to authenticate with available sources
116 125 attrs = AuthSource.authenticate(login, password)
117 126 if attrs
118 127 user = new(attrs)
119 128 user.login = login
120 129 user.language = Setting.default_language
121 130 if user.save
122 131 user.reload
123 132 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
124 133 end
125 134 end
126 135 end
127 136 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
128 137 user
129 138 rescue => text
130 139 raise text
131 140 end
132 141
133 142 # Returns the user who matches the given autologin +key+ or nil
134 143 def self.try_to_autologin(key)
135 144 tokens = Token.find_all_by_action_and_value('autologin', key)
136 145 # Make sure there's only 1 token that matches the key
137 146 if tokens.size == 1
138 147 token = tokens.first
139 148 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
140 149 token.user.update_attribute(:last_login_on, Time.now)
141 150 token.user
142 151 end
143 152 end
144 153 end
145 154
146 155 # Return user's full name for display
147 156 def name(formatter = nil)
148 157 if formatter
149 158 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
150 159 else
151 160 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
152 161 end
153 162 end
154 163
155 164 def active?
156 165 self.status == STATUS_ACTIVE
157 166 end
158 167
159 168 def registered?
160 169 self.status == STATUS_REGISTERED
161 170 end
162 171
163 172 def locked?
164 173 self.status == STATUS_LOCKED
165 174 end
166 175
167 176 def activate
168 177 self.status = STATUS_ACTIVE
169 178 end
170 179
171 180 def register
172 181 self.status = STATUS_REGISTERED
173 182 end
174 183
175 184 def lock
176 185 self.status = STATUS_LOCKED
177 186 end
178 187
179 188 def activate!
180 189 update_attribute(:status, STATUS_ACTIVE)
181 190 end
182 191
183 192 def register!
184 193 update_attribute(:status, STATUS_REGISTERED)
185 194 end
186 195
187 196 def lock!
188 197 update_attribute(:status, STATUS_LOCKED)
189 198 end
190 199
191 200 def check_password?(clear_password)
192 201 if auth_source_id.present?
193 202 auth_source.authenticate(self.login, clear_password)
194 203 else
195 204 User.hash_password(clear_password) == self.hashed_password
196 205 end
197 206 end
198 207
199 208 # Does the backend storage allow this user to change their password?
200 209 def change_password_allowed?
201 210 return true if auth_source_id.blank?
202 211 return auth_source.allow_password_changes?
203 212 end
204 213
205 214 # Generate and set a random password. Useful for automated user creation
206 215 # Based on Token#generate_token_value
207 216 #
208 217 def random_password
209 218 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
210 219 password = ''
211 220 40.times { |i| password << chars[rand(chars.size-1)] }
212 221 self.password = password
213 222 self.password_confirmation = password
214 223 self
215 224 end
216 225
217 226 def pref
218 227 self.preference ||= UserPreference.new(:user => self)
219 228 end
220 229
221 230 def time_zone
222 231 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
223 232 end
224 233
225 234 def wants_comments_in_reverse_order?
226 235 self.pref[:comments_sorting] == 'desc'
227 236 end
228 237
229 238 # Return user's RSS key (a 40 chars long string), used to access feeds
230 239 def rss_key
231 240 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
232 241 token.value
233 242 end
234 243
235 244 # Return user's API key (a 40 chars long string), used to access the API
236 245 def api_key
237 246 token = self.api_token || self.create_api_token(:action => 'api')
238 247 token.value
239 248 end
240 249
241 250 # Return an array of project ids for which the user has explicitly turned mail notifications on
242 251 def notified_projects_ids
243 252 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
244 253 end
245 254
246 255 def notified_project_ids=(ids)
247 256 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
248 257 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
249 258 @notified_projects_ids = nil
250 259 notified_projects_ids
251 260 end
252 261
253 262 # Find a user account by matching the exact login and then a case-insensitive
254 263 # version. Exact matches will be given priority.
255 264 def self.find_by_login(login)
256 265 # force string comparison to be case sensitive on MySQL
257 266 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
258 267
259 268 # First look for an exact match
260 269 user = first(:conditions => ["#{type_cast} login = ?", login])
261 270 # Fail over to case-insensitive if none was found
262 271 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
263 272 end
264 273
265 274 def self.find_by_rss_key(key)
266 275 token = Token.find_by_value(key)
267 276 token && token.user.active? ? token.user : nil
268 277 end
269 278
270 279 def self.find_by_api_key(key)
271 280 token = Token.find_by_action_and_value('api', key)
272 281 token && token.user.active? ? token.user : nil
273 282 end
274 283
275 284 # Makes find_by_mail case-insensitive
276 285 def self.find_by_mail(mail)
277 286 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
278 287 end
279 288
280 289 def to_s
281 290 name
282 291 end
283 292
284 293 # Returns the current day according to user's time zone
285 294 def today
286 295 if time_zone.nil?
287 296 Date.today
288 297 else
289 298 Time.now.in_time_zone(time_zone).to_date
290 299 end
291 300 end
292 301
293 302 def logged?
294 303 true
295 304 end
296 305
297 306 def anonymous?
298 307 !logged?
299 308 end
300 309
301 310 # Return user's roles for project
302 311 def roles_for_project(project)
303 312 roles = []
304 313 # No role on archived projects
305 314 return roles unless project && project.active?
306 315 if logged?
307 316 # Find project membership
308 317 membership = memberships.detect {|m| m.project_id == project.id}
309 318 if membership
310 319 roles = membership.roles
311 320 else
312 321 @role_non_member ||= Role.non_member
313 322 roles << @role_non_member
314 323 end
315 324 else
316 325 @role_anonymous ||= Role.anonymous
317 326 roles << @role_anonymous
318 327 end
319 328 roles
320 329 end
321 330
322 331 # Return true if the user is a member of project
323 332 def member_of?(project)
324 333 !roles_for_project(project).detect {|role| role.member?}.nil?
325 334 end
326 335
327 336 # Return true if the user is allowed to do the specified action on project
328 337 # action can be:
329 338 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
330 339 # * a permission Symbol (eg. :edit_project)
331 340 def allowed_to?(action, project, options={})
332 341 if project
333 342 # No action allowed on archived projects
334 343 return false unless project.active?
335 344 # No action allowed on disabled modules
336 345 return false unless project.allows_to?(action)
337 346 # Admin users are authorized for anything else
338 347 return true if admin?
339 348
340 349 roles = roles_for_project(project)
341 350 return false unless roles
342 351 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
343 352
344 353 elsif options[:global]
345 354 # Admin users are always authorized
346 355 return true if admin?
347 356
348 357 # authorize if user has at least one role that has this permission
349 358 roles = memberships.collect {|m| m.roles}.flatten.uniq
350 359 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
351 360 else
352 361 false
353 362 end
354 363 end
355 364
356 365 # Is the user allowed to do the specified action on any project?
357 366 # See allowed_to? for the actions and valid options.
358 367 def allowed_to_globally?(action, options)
359 368 allowed_to?(action, nil, options.reverse_merge(:global => true))
360 369 end
361 370
362 371 def self.current=(user)
363 372 @current_user = user
364 373 end
365 374
366 375 def self.current
367 376 @current_user ||= User.anonymous
368 377 end
369 378
370 379 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
371 380 # one anonymous user per database.
372 381 def self.anonymous
373 382 anonymous_user = AnonymousUser.find(:first)
374 383 if anonymous_user.nil?
375 384 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
376 385 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
377 386 end
378 387 anonymous_user
379 388 end
380 389
381 390 protected
382 391
383 392 def validate
384 393 # Password length validation based on setting
385 394 if !password.nil? && password.size < Setting.password_min_length.to_i
386 395 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
387 396 end
388 397 end
389 398
390 399 private
391 400
392 401 # Return password digest
393 402 def self.hash_password(clear_password)
394 403 Digest::SHA1.hexdigest(clear_password || "")
395 404 end
396 405 end
397 406
398 407 class AnonymousUser < User
399 408
400 409 def validate_on_create
401 410 # There should be only one AnonymousUser in the database
402 411 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
403 412 end
404 413
405 414 def available_custom_fields
406 415 []
407 416 end
408 417
409 418 # Overrides a few properties
410 419 def logged?; false end
411 420 def admin; false end
412 421 def name(*args); I18n.t(:label_user_anonymous) end
413 422 def mail; nil end
414 423 def time_zone; nil end
415 424 def rss_key; nil end
416 425 end
@@ -1,62 +1,62
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_change_password), :action => 'password') if @user.change_password_allowed? %>
3 3 <%= call_hook(:view_my_account_contextual, :user => @user)%>
4 4 </div>
5 5 <h2><%=l(:label_my_account)%></h2>
6 6 <%= error_messages_for 'user' %>
7 7
8 8 <% form_for :user, @user, :url => { :action => "account" },
9 9 :builder => TabularFormBuilder,
10 10 :lang => current_language,
11 11 :html => { :id => 'my_account_form' } do |f| %>
12 12 <div class="splitcontentleft">
13 13 <h3><%=l(:label_information_plural)%></h3>
14 14 <div class="box tabular">
15 15 <p><%= f.text_field :firstname, :required => true %></p>
16 16 <p><%= f.text_field :lastname, :required => true %></p>
17 17 <p><%= f.text_field :mail, :required => true %></p>
18 18 <p><%= f.select :language, lang_options_for_select %></p>
19 19 <% if Setting.openid? %>
20 20 <p><%= f.text_field :identity_url %></p>
21 21 <% end %>
22 22
23 23 <% @user.custom_field_values.select(&:editable?).each do |value| %>
24 24 <p><%= custom_field_tag_with_label :user, value %></p>
25 25 <% end %>
26 26 <%= call_hook(:view_my_account, :user => @user, :form => f) %>
27 27 </div>
28 28
29 29 <%= submit_tag l(:button_save) %>
30 30 </div>
31 31
32 32 <div class="splitcontentright">
33 33 <h3><%=l(:field_mail_notification)%></h3>
34 34 <div class="box">
35 <%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option),
35 <%= select_tag 'notification_option', options_for_select(@notification_options.collect {|o| [l(o.last), o.first]}, @notification_option.to_sym),
36 36 :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %>
37 37 <% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>
38 38 <p><% User.current.projects.each do |project| %>
39 39 <label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br />
40 40 <% end %></p>
41 41 <p><em><%= l(:text_user_mail_option) %></em></p>
42 42 <% end %>
43 43 <p><label><%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %> <%= l(:label_user_mail_no_self_notified) %></label></p>
44 44 </div>
45 45
46 46 <h3><%=l(:label_preferences)%></h3>
47 47 <div class="box tabular">
48 48 <% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %>
49 49 <p><%= pref_fields.check_box :hide_mail %></p>
50 50 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
51 51 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
52 52 <% end %>
53 53 </div>
54 54
55 55 </div>
56 56 <% end %>
57 57
58 58 <% content_for :sidebar do %>
59 59 <%= render :partial => 'sidebar' %>
60 60 <% end %>
61 61
62 62 <% html_title(l(:label_my_account)) -%>
@@ -1,920 +1,923
1 1 en:
2 2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 3 direction: ltr
4 4 date:
5 5 formats:
6 6 # Use the strftime parameters for formats.
7 7 # When no format has been given, it uses default.
8 8 # You can provide other formats here if you like!
9 9 default: "%m/%d/%Y"
10 10 short: "%b %d"
11 11 long: "%B %d, %Y"
12 12
13 13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15 15
16 16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 19 # Used in date_select and datime_select.
20 20 order: [ :year, :month, :day ]
21 21
22 22 time:
23 23 formats:
24 24 default: "%m/%d/%Y %I:%M %p"
25 25 time: "%I:%M %p"
26 26 short: "%d %b %H:%M"
27 27 long: "%B %d, %Y %H:%M"
28 28 am: "am"
29 29 pm: "pm"
30 30
31 31 datetime:
32 32 distance_in_words:
33 33 half_a_minute: "half a minute"
34 34 less_than_x_seconds:
35 35 one: "less than 1 second"
36 36 other: "less than {{count}} seconds"
37 37 x_seconds:
38 38 one: "1 second"
39 39 other: "{{count}} seconds"
40 40 less_than_x_minutes:
41 41 one: "less than a minute"
42 42 other: "less than {{count}} minutes"
43 43 x_minutes:
44 44 one: "1 minute"
45 45 other: "{{count}} minutes"
46 46 about_x_hours:
47 47 one: "about 1 hour"
48 48 other: "about {{count}} hours"
49 49 x_days:
50 50 one: "1 day"
51 51 other: "{{count}} days"
52 52 about_x_months:
53 53 one: "about 1 month"
54 54 other: "about {{count}} months"
55 55 x_months:
56 56 one: "1 month"
57 57 other: "{{count}} months"
58 58 about_x_years:
59 59 one: "about 1 year"
60 60 other: "about {{count}} years"
61 61 over_x_years:
62 62 one: "over 1 year"
63 63 other: "over {{count}} years"
64 64 almost_x_years:
65 65 one: "almost 1 year"
66 66 other: "almost {{count}} years"
67 67
68 68 number:
69 69 # Default format for numbers
70 70 format:
71 71 separator: "."
72 72 delimiter: ""
73 73 precision: 3
74 74 human:
75 75 format:
76 76 delimiter: ""
77 77 precision: 1
78 78 storage_units:
79 79 format: "%n %u"
80 80 units:
81 81 byte:
82 82 one: "Byte"
83 83 other: "Bytes"
84 84 kb: "KB"
85 85 mb: "MB"
86 86 gb: "GB"
87 87 tb: "TB"
88 88
89 89
90 90 # Used in array.to_sentence.
91 91 support:
92 92 array:
93 93 sentence_connector: "and"
94 94 skip_last_comma: false
95 95
96 96 activerecord:
97 97 errors:
98 98 messages:
99 99 inclusion: "is not included in the list"
100 100 exclusion: "is reserved"
101 101 invalid: "is invalid"
102 102 confirmation: "doesn't match confirmation"
103 103 accepted: "must be accepted"
104 104 empty: "can't be empty"
105 105 blank: "can't be blank"
106 106 too_long: "is too long (maximum is {{count}} characters)"
107 107 too_short: "is too short (minimum is {{count}} characters)"
108 108 wrong_length: "is the wrong length (should be {{count}} characters)"
109 109 taken: "has already been taken"
110 110 not_a_number: "is not a number"
111 111 not_a_date: "is not a valid date"
112 112 greater_than: "must be greater than {{count}}"
113 113 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
114 114 equal_to: "must be equal to {{count}}"
115 115 less_than: "must be less than {{count}}"
116 116 less_than_or_equal_to: "must be less than or equal to {{count}}"
117 117 odd: "must be odd"
118 118 even: "must be even"
119 119 greater_than_start_date: "must be greater than start date"
120 120 not_same_project: "doesn't belong to the same project"
121 121 circular_dependency: "This relation would create a circular dependency"
122 122 cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks"
123 123
124 124 actionview_instancetag_blank_option: Please select
125 125
126 126 general_text_No: 'No'
127 127 general_text_Yes: 'Yes'
128 128 general_text_no: 'no'
129 129 general_text_yes: 'yes'
130 130 general_lang_name: 'English'
131 131 general_csv_separator: ','
132 132 general_csv_decimal_separator: '.'
133 133 general_csv_encoding: ISO-8859-1
134 134 general_pdf_encoding: ISO-8859-1
135 135 general_first_day_of_week: '7'
136 136
137 137 notice_account_updated: Account was successfully updated.
138 138 notice_account_invalid_creditentials: Invalid user or password
139 139 notice_account_password_updated: Password was successfully updated.
140 140 notice_account_wrong_password: Wrong password
141 141 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
142 142 notice_account_unknown_email: Unknown user.
143 143 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
144 144 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
145 145 notice_account_activated: Your account has been activated. You can now log in.
146 146 notice_successful_create: Successful creation.
147 147 notice_successful_update: Successful update.
148 148 notice_successful_delete: Successful deletion.
149 149 notice_successful_connection: Successful connection.
150 150 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
151 151 notice_locking_conflict: Data has been updated by another user.
152 152 notice_not_authorized: You are not authorized to access this page.
153 153 notice_email_sent: "An email was sent to {{value}}"
154 154 notice_email_error: "An error occurred while sending mail ({{value}})"
155 155 notice_feeds_access_key_reseted: Your RSS access key was reset.
156 156 notice_api_access_key_reseted: Your API access key was reset.
157 157 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
158 158 notice_failed_to_save_members: "Failed to save member(s): {{errors}}."
159 159 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
160 160 notice_account_pending: "Your account was created and is now pending administrator approval."
161 161 notice_default_data_loaded: Default configuration successfully loaded.
162 162 notice_unable_delete_version: Unable to delete version.
163 163 notice_unable_delete_time_entry: Unable to delete time log entry.
164 164 notice_issue_done_ratios_updated: Issue done ratios updated.
165 165
166 166 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
167 167 error_scm_not_found: "The entry or revision was not found in the repository."
168 168 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
169 169 error_scm_annotate: "The entry does not exist or can not be annotated."
170 170 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
171 171 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
172 172 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
173 173 error_can_not_delete_custom_field: Unable to delete custom field
174 174 error_can_not_delete_tracker: "This tracker contains issues and can't be deleted."
175 175 error_can_not_remove_role: "This role is in use and can not be deleted."
176 176 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
177 177 error_can_not_archive_project: This project can not be archived
178 178 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
179 179 error_workflow_copy_source: 'Please select a source tracker or role'
180 180 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
181 181 error_unable_delete_issue_status: 'Unable to delete issue status'
182 182 error_unable_to_connect: "Unable to connect ({{value}})"
183 183 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
184 184
185 185 mail_subject_lost_password: "Your {{value}} password"
186 186 mail_body_lost_password: 'To change your password, click on the following link:'
187 187 mail_subject_register: "Your {{value}} account activation"
188 188 mail_body_register: 'To activate your account, click on the following link:'
189 189 mail_body_account_information_external: "You can use your {{value}} account to log in."
190 190 mail_body_account_information: Your account information
191 191 mail_subject_account_activation_request: "{{value}} account activation request"
192 192 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
193 193 mail_subject_reminder: "{{count}} issue(s) due in the next {{days}} days"
194 194 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
195 195 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
196 196 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
197 197 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
198 198 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
199 199
200 200 gui_validation_error: 1 error
201 201 gui_validation_error_plural: "{{count}} errors"
202 202
203 203 field_name: Name
204 204 field_description: Description
205 205 field_summary: Summary
206 206 field_is_required: Required
207 207 field_firstname: Firstname
208 208 field_lastname: Lastname
209 209 field_mail: Email
210 210 field_filename: File
211 211 field_filesize: Size
212 212 field_downloads: Downloads
213 213 field_author: Author
214 214 field_created_on: Created
215 215 field_updated_on: Updated
216 216 field_field_format: Format
217 217 field_is_for_all: For all projects
218 218 field_possible_values: Possible values
219 219 field_regexp: Regular expression
220 220 field_min_length: Minimum length
221 221 field_max_length: Maximum length
222 222 field_value: Value
223 223 field_category: Category
224 224 field_title: Title
225 225 field_project: Project
226 226 field_issue: Issue
227 227 field_status: Status
228 228 field_notes: Notes
229 229 field_is_closed: Issue closed
230 230 field_is_default: Default value
231 231 field_tracker: Tracker
232 232 field_subject: Subject
233 233 field_due_date: Due date
234 234 field_assigned_to: Assignee
235 235 field_priority: Priority
236 236 field_fixed_version: Target version
237 237 field_user: User
238 238 field_principal: Principal
239 239 field_role: Role
240 240 field_homepage: Homepage
241 241 field_is_public: Public
242 242 field_parent: Subproject of
243 243 field_is_in_roadmap: Issues displayed in roadmap
244 244 field_login: Login
245 245 field_mail_notification: Email notifications
246 246 field_admin: Administrator
247 247 field_last_login_on: Last connection
248 248 field_language: Language
249 249 field_effective_date: Date
250 250 field_password: Password
251 251 field_new_password: New password
252 252 field_password_confirmation: Confirmation
253 253 field_version: Version
254 254 field_type: Type
255 255 field_host: Host
256 256 field_port: Port
257 257 field_account: Account
258 258 field_base_dn: Base DN
259 259 field_attr_login: Login attribute
260 260 field_attr_firstname: Firstname attribute
261 261 field_attr_lastname: Lastname attribute
262 262 field_attr_mail: Email attribute
263 263 field_onthefly: On-the-fly user creation
264 264 field_start_date: Start
265 265 field_done_ratio: % Done
266 266 field_auth_source: Authentication mode
267 267 field_hide_mail: Hide my email address
268 268 field_comments: Comment
269 269 field_url: URL
270 270 field_start_page: Start page
271 271 field_subproject: Subproject
272 272 field_hours: Hours
273 273 field_activity: Activity
274 274 field_spent_on: Date
275 275 field_identifier: Identifier
276 276 field_is_filter: Used as a filter
277 277 field_issue_to: Related issue
278 278 field_delay: Delay
279 279 field_assignable: Issues can be assigned to this role
280 280 field_redirect_existing_links: Redirect existing links
281 281 field_estimated_hours: Estimated time
282 282 field_column_names: Columns
283 283 field_time_entries: Log time
284 284 field_time_zone: Time zone
285 285 field_searchable: Searchable
286 286 field_default_value: Default value
287 287 field_comments_sorting: Display comments
288 288 field_parent_title: Parent page
289 289 field_editable: Editable
290 290 field_watcher: Watcher
291 291 field_identity_url: OpenID URL
292 292 field_content: Content
293 293 field_group_by: Group results by
294 294 field_sharing: Sharing
295 295 field_parent_issue: Parent task
296 296 field_member_of_group: Member of Group
297 297 field_assigned_to_role: Member of Role
298 298 field_text: Text field
299 299
300 300 setting_app_title: Application title
301 301 setting_app_subtitle: Application subtitle
302 302 setting_welcome_text: Welcome text
303 303 setting_default_language: Default language
304 304 setting_login_required: Authentication required
305 305 setting_self_registration: Self-registration
306 306 setting_attachment_max_size: Attachment max. size
307 307 setting_issues_export_limit: Issues export limit
308 308 setting_mail_from: Emission email address
309 309 setting_bcc_recipients: Blind carbon copy recipients (bcc)
310 310 setting_plain_text_mail: Plain text mail (no HTML)
311 311 setting_host_name: Host name and path
312 312 setting_text_formatting: Text formatting
313 313 setting_wiki_compression: Wiki history compression
314 314 setting_feeds_limit: Feed content limit
315 315 setting_default_projects_public: New projects are public by default
316 316 setting_autofetch_changesets: Autofetch commits
317 317 setting_sys_api_enabled: Enable WS for repository management
318 318 setting_commit_ref_keywords: Referencing keywords
319 319 setting_commit_fix_keywords: Fixing keywords
320 320 setting_autologin: Autologin
321 321 setting_date_format: Date format
322 322 setting_time_format: Time format
323 323 setting_cross_project_issue_relations: Allow cross-project issue relations
324 324 setting_issue_list_default_columns: Default columns displayed on the issue list
325 325 setting_repositories_encodings: Repositories encodings
326 326 setting_commit_logs_encoding: Commit messages encoding
327 327 setting_emails_footer: Emails footer
328 328 setting_protocol: Protocol
329 329 setting_per_page_options: Objects per page options
330 330 setting_user_format: Users display format
331 331 setting_activity_days_default: Days displayed on project activity
332 332 setting_display_subprojects_issues: Display subprojects issues on main projects by default
333 333 setting_enabled_scm: Enabled SCM
334 334 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
335 335 setting_mail_handler_api_enabled: Enable WS for incoming emails
336 336 setting_mail_handler_api_key: API key
337 337 setting_sequential_project_identifiers: Generate sequential project identifiers
338 338 setting_gravatar_enabled: Use Gravatar user icons
339 339 setting_gravatar_default: Default Gravatar image
340 340 setting_diff_max_lines_displayed: Max number of diff lines displayed
341 341 setting_file_max_size_displayed: Max size of text files displayed inline
342 342 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
343 343 setting_openid: Allow OpenID login and registration
344 344 setting_password_min_length: Minimum password length
345 345 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
346 346 setting_default_projects_modules: Default enabled modules for new projects
347 347 setting_issue_done_ratio: Calculate the issue done ratio with
348 348 setting_issue_done_ratio_issue_field: Use the issue field
349 349 setting_issue_done_ratio_issue_status: Use the issue status
350 350 setting_start_of_week: Start calendars on
351 351 setting_rest_api_enabled: Enable REST web service
352 352 setting_cache_formatted_text: Cache formatted text
353 353
354 354 permission_add_project: Create project
355 355 permission_add_subprojects: Create subprojects
356 356 permission_edit_project: Edit project
357 357 permission_select_project_modules: Select project modules
358 358 permission_manage_members: Manage members
359 359 permission_manage_project_activities: Manage project activities
360 360 permission_manage_versions: Manage versions
361 361 permission_manage_categories: Manage issue categories
362 362 permission_view_issues: View Issues
363 363 permission_add_issues: Add issues
364 364 permission_edit_issues: Edit issues
365 365 permission_manage_issue_relations: Manage issue relations
366 366 permission_add_issue_notes: Add notes
367 367 permission_edit_issue_notes: Edit notes
368 368 permission_edit_own_issue_notes: Edit own notes
369 369 permission_move_issues: Move issues
370 370 permission_delete_issues: Delete issues
371 371 permission_manage_public_queries: Manage public queries
372 372 permission_save_queries: Save queries
373 373 permission_view_gantt: View gantt chart
374 374 permission_view_calendar: View calendar
375 375 permission_view_issue_watchers: View watchers list
376 376 permission_add_issue_watchers: Add watchers
377 377 permission_delete_issue_watchers: Delete watchers
378 378 permission_log_time: Log spent time
379 379 permission_view_time_entries: View spent time
380 380 permission_edit_time_entries: Edit time logs
381 381 permission_edit_own_time_entries: Edit own time logs
382 382 permission_manage_news: Manage news
383 383 permission_comment_news: Comment news
384 384 permission_manage_documents: Manage documents
385 385 permission_view_documents: View documents
386 386 permission_manage_files: Manage files
387 387 permission_view_files: View files
388 388 permission_manage_wiki: Manage wiki
389 389 permission_rename_wiki_pages: Rename wiki pages
390 390 permission_delete_wiki_pages: Delete wiki pages
391 391 permission_view_wiki_pages: View wiki
392 392 permission_view_wiki_edits: View wiki history
393 393 permission_edit_wiki_pages: Edit wiki pages
394 394 permission_delete_wiki_pages_attachments: Delete attachments
395 395 permission_protect_wiki_pages: Protect wiki pages
396 396 permission_manage_repository: Manage repository
397 397 permission_browse_repository: Browse repository
398 398 permission_view_changesets: View changesets
399 399 permission_commit_access: Commit access
400 400 permission_manage_boards: Manage boards
401 401 permission_view_messages: View messages
402 402 permission_add_messages: Post messages
403 403 permission_edit_messages: Edit messages
404 404 permission_edit_own_messages: Edit own messages
405 405 permission_delete_messages: Delete messages
406 406 permission_delete_own_messages: Delete own messages
407 407 permission_export_wiki_pages: Export wiki pages
408 408 permission_manage_subtasks: Manage subtasks
409 409
410 410 project_module_issue_tracking: Issue tracking
411 411 project_module_time_tracking: Time tracking
412 412 project_module_news: News
413 413 project_module_documents: Documents
414 414 project_module_files: Files
415 415 project_module_wiki: Wiki
416 416 project_module_repository: Repository
417 417 project_module_boards: Boards
418 418 project_module_calendar: Calendar
419 419 project_module_gantt: Gantt
420 420
421 421 label_user: User
422 422 label_user_plural: Users
423 423 label_user_new: New user
424 424 label_user_anonymous: Anonymous
425 425 label_project: Project
426 426 label_project_new: New project
427 427 label_project_plural: Projects
428 428 label_x_projects:
429 429 zero: no projects
430 430 one: 1 project
431 431 other: "{{count}} projects"
432 432 label_project_all: All Projects
433 433 label_project_latest: Latest projects
434 434 label_issue: Issue
435 435 label_issue_new: New issue
436 436 label_issue_plural: Issues
437 437 label_issue_view_all: View all issues
438 438 label_issues_by: "Issues by {{value}}"
439 439 label_issue_added: Issue added
440 440 label_issue_updated: Issue updated
441 441 label_document: Document
442 442 label_document_new: New document
443 443 label_document_plural: Documents
444 444 label_document_added: Document added
445 445 label_role: Role
446 446 label_role_plural: Roles
447 447 label_role_new: New role
448 448 label_role_and_permissions: Roles and permissions
449 449 label_member: Member
450 450 label_member_new: New member
451 451 label_member_plural: Members
452 452 label_tracker: Tracker
453 453 label_tracker_plural: Trackers
454 454 label_tracker_new: New tracker
455 455 label_workflow: Workflow
456 456 label_issue_status: Issue status
457 457 label_issue_status_plural: Issue statuses
458 458 label_issue_status_new: New status
459 459 label_issue_category: Issue category
460 460 label_issue_category_plural: Issue categories
461 461 label_issue_category_new: New category
462 462 label_custom_field: Custom field
463 463 label_custom_field_plural: Custom fields
464 464 label_custom_field_new: New custom field
465 465 label_enumerations: Enumerations
466 466 label_enumeration_new: New value
467 467 label_information: Information
468 468 label_information_plural: Information
469 469 label_please_login: Please log in
470 470 label_register: Register
471 471 label_login_with_open_id_option: or login with OpenID
472 472 label_password_lost: Lost password
473 473 label_home: Home
474 474 label_my_page: My page
475 475 label_my_account: My account
476 476 label_my_projects: My projects
477 477 label_my_page_block: My page block
478 478 label_administration: Administration
479 479 label_login: Sign in
480 480 label_logout: Sign out
481 481 label_help: Help
482 482 label_reported_issues: Reported issues
483 483 label_assigned_to_me_issues: Issues assigned to me
484 484 label_last_login: Last connection
485 485 label_registered_on: Registered on
486 486 label_activity: Activity
487 487 label_overall_activity: Overall activity
488 488 label_user_activity: "{{value}}'s activity"
489 489 label_new: New
490 490 label_logged_as: Logged in as
491 491 label_environment: Environment
492 492 label_authentication: Authentication
493 493 label_auth_source: Authentication mode
494 494 label_auth_source_new: New authentication mode
495 495 label_auth_source_plural: Authentication modes
496 496 label_subproject_plural: Subprojects
497 497 label_subproject_new: New subproject
498 498 label_and_its_subprojects: "{{value}} and its subprojects"
499 499 label_min_max_length: Min - Max length
500 500 label_list: List
501 501 label_date: Date
502 502 label_integer: Integer
503 503 label_float: Float
504 504 label_boolean: Boolean
505 505 label_string: Text
506 506 label_text: Long text
507 507 label_attribute: Attribute
508 508 label_attribute_plural: Attributes
509 509 label_download: "{{count}} Download"
510 510 label_download_plural: "{{count}} Downloads"
511 511 label_no_data: No data to display
512 512 label_change_status: Change status
513 513 label_history: History
514 514 label_attachment: File
515 515 label_attachment_new: New file
516 516 label_attachment_delete: Delete file
517 517 label_attachment_plural: Files
518 518 label_file_added: File added
519 519 label_report: Report
520 520 label_report_plural: Reports
521 521 label_news: News
522 522 label_news_new: Add news
523 523 label_news_plural: News
524 524 label_news_latest: Latest news
525 525 label_news_view_all: View all news
526 526 label_news_added: News added
527 527 label_settings: Settings
528 528 label_overview: Overview
529 529 label_version: Version
530 530 label_version_new: New version
531 531 label_version_plural: Versions
532 532 label_close_versions: Close completed versions
533 533 label_confirmation: Confirmation
534 534 label_export_to: 'Also available in:'
535 535 label_read: Read...
536 536 label_public_projects: Public projects
537 537 label_open_issues: open
538 538 label_open_issues_plural: open
539 539 label_closed_issues: closed
540 540 label_closed_issues_plural: closed
541 541 label_x_open_issues_abbr_on_total:
542 542 zero: 0 open / {{total}}
543 543 one: 1 open / {{total}}
544 544 other: "{{count}} open / {{total}}"
545 545 label_x_open_issues_abbr:
546 546 zero: 0 open
547 547 one: 1 open
548 548 other: "{{count}} open"
549 549 label_x_closed_issues_abbr:
550 550 zero: 0 closed
551 551 one: 1 closed
552 552 other: "{{count}} closed"
553 553 label_total: Total
554 554 label_permissions: Permissions
555 555 label_current_status: Current status
556 556 label_new_statuses_allowed: New statuses allowed
557 557 label_all: all
558 558 label_none: none
559 559 label_nobody: nobody
560 560 label_next: Next
561 561 label_previous: Previous
562 562 label_used_by: Used by
563 563 label_details: Details
564 564 label_add_note: Add a note
565 565 label_per_page: Per page
566 566 label_calendar: Calendar
567 567 label_months_from: months from
568 568 label_gantt: Gantt
569 569 label_internal: Internal
570 570 label_last_changes: "last {{count}} changes"
571 571 label_change_view_all: View all changes
572 572 label_personalize_page: Personalize this page
573 573 label_comment: Comment
574 574 label_comment_plural: Comments
575 575 label_x_comments:
576 576 zero: no comments
577 577 one: 1 comment
578 578 other: "{{count}} comments"
579 579 label_comment_add: Add a comment
580 580 label_comment_added: Comment added
581 581 label_comment_delete: Delete comments
582 582 label_query: Custom query
583 583 label_query_plural: Custom queries
584 584 label_query_new: New query
585 585 label_filter_add: Add filter
586 586 label_filter_plural: Filters
587 587 label_equals: is
588 588 label_not_equals: is not
589 589 label_in_less_than: in less than
590 590 label_in_more_than: in more than
591 591 label_greater_or_equal: '>='
592 592 label_less_or_equal: '<='
593 593 label_in: in
594 594 label_today: today
595 595 label_all_time: all time
596 596 label_yesterday: yesterday
597 597 label_this_week: this week
598 598 label_last_week: last week
599 599 label_last_n_days: "last {{count}} days"
600 600 label_this_month: this month
601 601 label_last_month: last month
602 602 label_this_year: this year
603 603 label_date_range: Date range
604 604 label_less_than_ago: less than days ago
605 605 label_more_than_ago: more than days ago
606 606 label_ago: days ago
607 607 label_contains: contains
608 608 label_not_contains: doesn't contain
609 609 label_day_plural: days
610 610 label_repository: Repository
611 611 label_repository_plural: Repositories
612 612 label_browse: Browse
613 613 label_modification: "{{count}} change"
614 614 label_modification_plural: "{{count}} changes"
615 615 label_branch: Branch
616 616 label_tag: Tag
617 617 label_revision: Revision
618 618 label_revision_plural: Revisions
619 619 label_revision_id: "Revision {{value}}"
620 620 label_associated_revisions: Associated revisions
621 621 label_added: added
622 622 label_modified: modified
623 623 label_copied: copied
624 624 label_renamed: renamed
625 625 label_deleted: deleted
626 626 label_latest_revision: Latest revision
627 627 label_latest_revision_plural: Latest revisions
628 628 label_view_revisions: View revisions
629 629 label_view_all_revisions: View all revisions
630 630 label_max_size: Maximum size
631 631 label_sort_highest: Move to top
632 632 label_sort_higher: Move up
633 633 label_sort_lower: Move down
634 634 label_sort_lowest: Move to bottom
635 635 label_roadmap: Roadmap
636 636 label_roadmap_due_in: "Due in {{value}}"
637 637 label_roadmap_overdue: "{{value}} late"
638 638 label_roadmap_no_issues: No issues for this version
639 639 label_search: Search
640 640 label_result_plural: Results
641 641 label_all_words: All words
642 642 label_wiki: Wiki
643 643 label_wiki_edit: Wiki edit
644 644 label_wiki_edit_plural: Wiki edits
645 645 label_wiki_page: Wiki page
646 646 label_wiki_page_plural: Wiki pages
647 647 label_index_by_title: Index by title
648 648 label_index_by_date: Index by date
649 649 label_current_version: Current version
650 650 label_preview: Preview
651 651 label_feed_plural: Feeds
652 652 label_changes_details: Details of all changes
653 653 label_issue_tracking: Issue tracking
654 654 label_spent_time: Spent time
655 655 label_overall_spent_time: Overall spent time
656 656 label_f_hour: "{{value}} hour"
657 657 label_f_hour_plural: "{{value}} hours"
658 658 label_time_tracking: Time tracking
659 659 label_change_plural: Changes
660 660 label_statistics: Statistics
661 661 label_commits_per_month: Commits per month
662 662 label_commits_per_author: Commits per author
663 663 label_view_diff: View differences
664 664 label_diff_inline: inline
665 665 label_diff_side_by_side: side by side
666 666 label_options: Options
667 667 label_copy_workflow_from: Copy workflow from
668 668 label_permissions_report: Permissions report
669 669 label_watched_issues: Watched issues
670 670 label_related_issues: Related issues
671 671 label_applied_status: Applied status
672 672 label_loading: Loading...
673 673 label_relation_new: New relation
674 674 label_relation_delete: Delete relation
675 675 label_relates_to: related to
676 676 label_duplicates: duplicates
677 677 label_duplicated_by: duplicated by
678 678 label_blocks: blocks
679 679 label_blocked_by: blocked by
680 680 label_precedes: precedes
681 681 label_follows: follows
682 682 label_end_to_start: end to start
683 683 label_end_to_end: end to end
684 684 label_start_to_start: start to start
685 685 label_start_to_end: start to end
686 686 label_stay_logged_in: Stay logged in
687 687 label_disabled: disabled
688 688 label_show_completed_versions: Show completed versions
689 689 label_me: me
690 690 label_board: Forum
691 691 label_board_new: New forum
692 692 label_board_plural: Forums
693 693 label_board_locked: Locked
694 694 label_board_sticky: Sticky
695 695 label_topic_plural: Topics
696 696 label_message_plural: Messages
697 697 label_message_last: Last message
698 698 label_message_new: New message
699 699 label_message_posted: Message added
700 700 label_reply_plural: Replies
701 701 label_send_information: Send account information to the user
702 702 label_year: Year
703 703 label_month: Month
704 704 label_week: Week
705 705 label_date_from: From
706 706 label_date_to: To
707 707 label_language_based: Based on user's language
708 708 label_sort_by: "Sort by {{value}}"
709 709 label_send_test_email: Send a test email
710 710 label_feeds_access_key: RSS access key
711 711 label_missing_feeds_access_key: Missing a RSS access key
712 712 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
713 713 label_module_plural: Modules
714 714 label_added_time_by: "Added by {{author}} {{age}} ago"
715 715 label_updated_time_by: "Updated by {{author}} {{age}} ago"
716 716 label_updated_time: "Updated {{value}} ago"
717 717 label_jump_to_a_project: Jump to a project...
718 718 label_file_plural: Files
719 719 label_changeset_plural: Changesets
720 720 label_default_columns: Default columns
721 721 label_no_change_option: (No change)
722 722 label_bulk_edit_selected_issues: Bulk edit selected issues
723 723 label_theme: Theme
724 724 label_default: Default
725 725 label_search_titles_only: Search titles only
726 726 label_user_mail_option_all: "For any event on all my projects"
727 727 label_user_mail_option_selected: "For any event on the selected projects only..."
728 label_user_mail_option_none: "Only for things I watch or I'm involved in"
728 label_user_mail_option_none: "No events"
729 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
730 label_user_mail_option_only_assigned: "Only for things I am assigned to"
731 label_user_mail_option_only_owner: "Only for things I am the owner of"
729 732 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
730 733 label_registration_activation_by_email: account activation by email
731 734 label_registration_manual_activation: manual account activation
732 735 label_registration_automatic_activation: automatic account activation
733 736 label_display_per_page: "Per page: {{value}}"
734 737 label_age: Age
735 738 label_change_properties: Change properties
736 739 label_general: General
737 740 label_more: More
738 741 label_scm: SCM
739 742 label_plugins: Plugins
740 743 label_ldap_authentication: LDAP authentication
741 744 label_downloads_abbr: D/L
742 745 label_optional_description: Optional description
743 746 label_add_another_file: Add another file
744 747 label_preferences: Preferences
745 748 label_chronological_order: In chronological order
746 749 label_reverse_chronological_order: In reverse chronological order
747 750 label_planning: Planning
748 751 label_incoming_emails: Incoming emails
749 752 label_generate_key: Generate a key
750 753 label_issue_watchers: Watchers
751 754 label_example: Example
752 755 label_display: Display
753 756 label_sort: Sort
754 757 label_ascending: Ascending
755 758 label_descending: Descending
756 759 label_date_from_to: From {{start}} to {{end}}
757 760 label_wiki_content_added: Wiki page added
758 761 label_wiki_content_updated: Wiki page updated
759 762 label_group: Group
760 763 label_group_plural: Groups
761 764 label_group_new: New group
762 765 label_time_entry_plural: Spent time
763 766 label_version_sharing_none: Not shared
764 767 label_version_sharing_descendants: With subprojects
765 768 label_version_sharing_hierarchy: With project hierarchy
766 769 label_version_sharing_tree: With project tree
767 770 label_version_sharing_system: With all projects
768 771 label_update_issue_done_ratios: Update issue done ratios
769 772 label_copy_source: Source
770 773 label_copy_target: Target
771 774 label_copy_same_as_target: Same as target
772 775 label_display_used_statuses_only: Only display statuses that are used by this tracker
773 776 label_api_access_key: API access key
774 777 label_missing_api_access_key: Missing an API access key
775 778 label_api_access_key_created_on: "API access key created {{value}} ago"
776 779 label_profile: Profile
777 780 label_subtask_plural: Subtasks
778 781 label_project_copy_notifications: Send email notifications during the project copy
779 782
780 783 button_login: Login
781 784 button_submit: Submit
782 785 button_save: Save
783 786 button_check_all: Check all
784 787 button_uncheck_all: Uncheck all
785 788 button_delete: Delete
786 789 button_create: Create
787 790 button_create_and_continue: Create and continue
788 791 button_test: Test
789 792 button_edit: Edit
790 793 button_edit_associated_wikipage: "Edit associated Wiki page: {{page_title}}"
791 794 button_add: Add
792 795 button_change: Change
793 796 button_apply: Apply
794 797 button_clear: Clear
795 798 button_lock: Lock
796 799 button_unlock: Unlock
797 800 button_download: Download
798 801 button_list: List
799 802 button_view: View
800 803 button_move: Move
801 804 button_move_and_follow: Move and follow
802 805 button_back: Back
803 806 button_cancel: Cancel
804 807 button_activate: Activate
805 808 button_sort: Sort
806 809 button_log_time: Log time
807 810 button_rollback: Rollback to this version
808 811 button_watch: Watch
809 812 button_unwatch: Unwatch
810 813 button_reply: Reply
811 814 button_archive: Archive
812 815 button_unarchive: Unarchive
813 816 button_reset: Reset
814 817 button_rename: Rename
815 818 button_change_password: Change password
816 819 button_copy: Copy
817 820 button_copy_and_follow: Copy and follow
818 821 button_annotate: Annotate
819 822 button_update: Update
820 823 button_configure: Configure
821 824 button_quote: Quote
822 825 button_duplicate: Duplicate
823 826 button_show: Show
824 827
825 828 status_active: active
826 829 status_registered: registered
827 830 status_locked: locked
828 831
829 832 version_status_open: open
830 833 version_status_locked: locked
831 834 version_status_closed: closed
832 835
833 836 field_active: Active
834 837
835 838 text_select_mail_notifications: Select actions for which email notifications should be sent.
836 839 text_regexp_info: eg. ^[A-Z0-9]+$
837 840 text_min_max_length_info: 0 means no restriction
838 841 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
839 842 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
840 843 text_workflow_edit: Select a role and a tracker to edit the workflow
841 844 text_are_you_sure: Are you sure ?
842 845 text_are_you_sure_with_children: "Delete issue and all child issues?"
843 846 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
844 847 text_journal_set_to: "{{label}} set to {{value}}"
845 848 text_journal_deleted: "{{label}} deleted ({{old}})"
846 849 text_journal_added: "{{label}} {{value}} added"
847 850 text_tip_task_begin_day: task beginning this day
848 851 text_tip_task_end_day: task ending this day
849 852 text_tip_task_begin_end_day: task beginning and ending this day
850 853 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
851 854 text_caracters_maximum: "{{count}} characters maximum."
852 855 text_caracters_minimum: "Must be at least {{count}} characters long."
853 856 text_length_between: "Length between {{min}} and {{max}} characters."
854 857 text_tracker_no_workflow: No workflow defined for this tracker
855 858 text_unallowed_characters: Unallowed characters
856 859 text_comma_separated: Multiple values allowed (comma separated).
857 860 text_line_separated: Multiple values allowed (one line for each value).
858 861 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
859 862 text_issue_added: "Issue {{id}} has been reported by {{author}}."
860 863 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
861 864 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
862 865 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
863 866 text_issue_category_destroy_assignments: Remove category assignments
864 867 text_issue_category_reassign_to: Reassign issues to this category
865 868 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
866 869 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
867 870 text_load_default_configuration: Load the default configuration
868 871 text_status_changed_by_changeset: "Applied in changeset {{value}}."
869 872 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
870 873 text_select_project_modules: 'Select modules to enable for this project:'
871 874 text_default_administrator_account_changed: Default administrator account changed
872 875 text_file_repository_writable: Attachments directory writable
873 876 text_plugin_assets_writable: Plugin assets directory writable
874 877 text_rmagick_available: RMagick available (optional)
875 878 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
876 879 text_destroy_time_entries: Delete reported hours
877 880 text_assign_time_entries_to_project: Assign reported hours to the project
878 881 text_reassign_time_entries: 'Reassign reported hours to this issue:'
879 882 text_user_wrote: "{{value}} wrote:"
880 883 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
881 884 text_enumeration_category_reassign_to: 'Reassign them to this value:'
882 885 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
883 886 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
884 887 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
885 888 text_custom_field_possible_values_info: 'One line for each value'
886 889 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
887 890 text_wiki_page_nullify_children: "Keep child pages as root pages"
888 891 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
889 892 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
890 893 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
891 894 text_zoom_in: Zoom in
892 895 text_zoom_out: Zoom out
893 896
894 897 default_role_manager: Manager
895 898 default_role_developer: Developer
896 899 default_role_reporter: Reporter
897 900 default_tracker_bug: Bug
898 901 default_tracker_feature: Feature
899 902 default_tracker_support: Support
900 903 default_issue_status_new: New
901 904 default_issue_status_in_progress: In Progress
902 905 default_issue_status_resolved: Resolved
903 906 default_issue_status_feedback: Feedback
904 907 default_issue_status_closed: Closed
905 908 default_issue_status_rejected: Rejected
906 909 default_doc_category_user: User documentation
907 910 default_doc_category_tech: Technical documentation
908 911 default_priority_low: Low
909 912 default_priority_normal: Normal
910 913 default_priority_high: High
911 914 default_priority_urgent: Urgent
912 915 default_priority_immediate: Immediate
913 916 default_activity_design: Design
914 917 default_activity_development: Development
915 918
916 919 enumeration_issue_priorities: Issue priorities
917 920 enumeration_doc_categories: Document categories
918 921 enumeration_activities: Activities (time tracking)
919 922 enumeration_system_activity: System Activity
920 923
@@ -1,156 +1,156
1 1 ---
2 2 users_004:
3 3 created_on: 2006-07-19 19:34:07 +02:00
4 4 status: 1
5 5 last_login_on:
6 6 language: en
7 7 hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608
8 8 updated_on: 2006-07-19 19:34:07 +02:00
9 9 admin: false
10 10 mail: rhill@somenet.foo
11 11 lastname: Hill
12 12 firstname: Robert
13 13 id: 4
14 14 auth_source_id:
15 mail_notification: true
15 mail_notification: all
16 16 login: rhill
17 17 type: User
18 18 users_001:
19 19 created_on: 2006-07-19 19:12:21 +02:00
20 20 status: 1
21 21 last_login_on: 2006-07-19 22:57:52 +02:00
22 22 language: en
23 23 hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997
24 24 updated_on: 2006-07-19 22:57:52 +02:00
25 25 admin: true
26 26 mail: admin@somenet.foo
27 27 lastname: Admin
28 28 firstname: redMine
29 29 id: 1
30 30 auth_source_id:
31 mail_notification: true
31 mail_notification: all
32 32 login: admin
33 33 type: User
34 34 users_002:
35 35 created_on: 2006-07-19 19:32:09 +02:00
36 36 status: 1
37 37 last_login_on: 2006-07-19 22:42:15 +02:00
38 38 language: en
39 39 hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d
40 40 updated_on: 2006-07-19 22:42:15 +02:00
41 41 admin: false
42 42 mail: jsmith@somenet.foo
43 43 lastname: Smith
44 44 firstname: John
45 45 id: 2
46 46 auth_source_id:
47 mail_notification: true
47 mail_notification: all
48 48 login: jsmith
49 49 type: User
50 50 users_003:
51 51 created_on: 2006-07-19 19:33:19 +02:00
52 52 status: 1
53 53 last_login_on:
54 54 language: en
55 55 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
56 56 updated_on: 2006-07-19 19:33:19 +02:00
57 57 admin: false
58 58 mail: dlopper@somenet.foo
59 59 lastname: Lopper
60 60 firstname: Dave
61 61 id: 3
62 62 auth_source_id:
63 mail_notification: true
63 mail_notification: all
64 64 login: dlopper
65 65 type: User
66 66 users_005:
67 67 id: 5
68 68 created_on: 2006-07-19 19:33:19 +02:00
69 69 # Locked
70 70 status: 3
71 71 last_login_on:
72 72 language: en
73 73 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
74 74 updated_on: 2006-07-19 19:33:19 +02:00
75 75 admin: false
76 76 mail: dlopper2@somenet.foo
77 77 lastname: Lopper2
78 78 firstname: Dave2
79 79 auth_source_id:
80 mail_notification: true
80 mail_notification: all
81 81 login: dlopper2
82 82 type: User
83 83 users_006:
84 84 id: 6
85 85 created_on: 2006-07-19 19:33:19 +02:00
86 86 status: 0
87 87 last_login_on:
88 88 language: ''
89 89 hashed_password: 1
90 90 updated_on: 2006-07-19 19:33:19 +02:00
91 91 admin: false
92 92 mail: ''
93 93 lastname: Anonymous
94 94 firstname: ''
95 95 auth_source_id:
96 mail_notification: false
96 mail_notification: only_my_events
97 97 login: ''
98 98 type: AnonymousUser
99 99 users_007:
100 100 id: 7
101 101 created_on: 2006-07-19 19:33:19 +02:00
102 102 status: 1
103 103 last_login_on:
104 104 language: ''
105 105 hashed_password: 1
106 106 updated_on: 2006-07-19 19:33:19 +02:00
107 107 admin: false
108 108 mail: someone@foo.bar
109 109 lastname: One
110 110 firstname: Some
111 111 auth_source_id:
112 mail_notification: false
112 mail_notification: only_my_events
113 113 login: someone
114 114 type: User
115 115 users_008:
116 116 id: 8
117 117 created_on: 2006-07-19 19:33:19 +02:00
118 118 status: 1
119 119 last_login_on:
120 120 language: 'it'
121 121 hashed_password: 1
122 122 updated_on: 2006-07-19 19:33:19 +02:00
123 123 admin: false
124 124 mail: miscuser8@foo.bar
125 125 lastname: Misc
126 126 firstname: User
127 127 auth_source_id:
128 mail_notification: false
128 mail_notification: only_my_events
129 129 login: miscuser8
130 130 type: User
131 131 users_009:
132 132 id: 9
133 133 created_on: 2006-07-19 19:33:19 +02:00
134 134 status: 1
135 135 last_login_on:
136 136 language: 'it'
137 137 hashed_password: 1
138 138 updated_on: 2006-07-19 19:33:19 +02:00
139 139 admin: false
140 140 mail: miscuser9@foo.bar
141 141 lastname: Misc
142 142 firstname: User
143 143 auth_source_id:
144 mail_notification: false
144 mail_notification: only_my_events
145 145 login: miscuser9
146 146 type: User
147 147 groups_010:
148 148 id: 10
149 149 lastname: A Team
150 150 type: Group
151 151 groups_011:
152 152 id: 11
153 153 lastname: B Team
154 154 type: Group
155 155
156 No newline at end of file
156
@@ -1,436 +1,436
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources
22 22
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 test 'object_daddy creation' do
30 30 User.generate_with_protected!(:firstname => 'Testing connection')
31 31 User.generate_with_protected!(:firstname => 'Testing connection')
32 32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
33 33 end
34 34
35 35 def test_truth
36 36 assert_kind_of User, @jsmith
37 37 end
38 38
39 39 def test_mail_should_be_stripped
40 40 u = User.new
41 41 u.mail = " foo@bar.com "
42 42 assert_equal "foo@bar.com", u.mail
43 43 end
44 44
45 45 def test_create
46 46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
47 47
48 48 user.login = "jsmith"
49 49 user.password, user.password_confirmation = "password", "password"
50 50 # login uniqueness
51 51 assert !user.save
52 52 assert_equal 1, user.errors.count
53 53
54 54 user.login = "newuser"
55 55 user.password, user.password_confirmation = "passwd", "password"
56 56 # password confirmation
57 57 assert !user.save
58 58 assert_equal 1, user.errors.count
59 59
60 60 user.password, user.password_confirmation = "password", "password"
61 61 assert user.save
62 62 end
63 63
64 64 context "User.login" do
65 65 should "be case-insensitive." do
66 66 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
67 67 u.login = 'newuser'
68 68 u.password, u.password_confirmation = "password", "password"
69 69 assert u.save
70 70
71 71 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
72 72 u.login = 'NewUser'
73 73 u.password, u.password_confirmation = "password", "password"
74 74 assert !u.save
75 75 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
76 76 end
77 77 end
78 78
79 79 def test_mail_uniqueness_should_not_be_case_sensitive
80 80 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
81 81 u.login = 'newuser1'
82 82 u.password, u.password_confirmation = "password", "password"
83 83 assert u.save
84 84
85 85 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
86 86 u.login = 'newuser2'
87 87 u.password, u.password_confirmation = "password", "password"
88 88 assert !u.save
89 89 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
90 90 end
91 91
92 92 def test_update
93 93 assert_equal "admin", @admin.login
94 94 @admin.login = "john"
95 95 assert @admin.save, @admin.errors.full_messages.join("; ")
96 96 @admin.reload
97 97 assert_equal "john", @admin.login
98 98 end
99 99
100 100 def test_destroy
101 101 User.find(2).destroy
102 102 assert_nil User.find_by_id(2)
103 103 assert Member.find_all_by_user_id(2).empty?
104 104 end
105 105
106 106 def test_validate
107 107 @admin.login = ""
108 108 assert !@admin.save
109 109 assert_equal 1, @admin.errors.count
110 110 end
111 111
112 112 context "User#try_to_login" do
113 113 should "fall-back to case-insensitive if user login is not found as-typed." do
114 114 user = User.try_to_login("AdMin", "admin")
115 115 assert_kind_of User, user
116 116 assert_equal "admin", user.login
117 117 end
118 118
119 119 should "select the exact matching user first" do
120 120 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
121 121 # bypass validations to make it appear like existing data
122 122 case_sensitive_user.update_attribute(:login, 'ADMIN')
123 123
124 124 user = User.try_to_login("ADMIN", "admin")
125 125 assert_kind_of User, user
126 126 assert_equal "ADMIN", user.login
127 127
128 128 end
129 129 end
130 130
131 131 def test_password
132 132 user = User.try_to_login("admin", "admin")
133 133 assert_kind_of User, user
134 134 assert_equal "admin", user.login
135 135 user.password = "hello"
136 136 assert user.save
137 137
138 138 user = User.try_to_login("admin", "hello")
139 139 assert_kind_of User, user
140 140 assert_equal "admin", user.login
141 141 assert_equal User.hash_password("hello"), user.hashed_password
142 142 end
143 143
144 144 def test_name_format
145 145 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
146 146 Setting.user_format = :firstname_lastname
147 147 assert_equal 'John Smith', @jsmith.reload.name
148 148 Setting.user_format = :username
149 149 assert_equal 'jsmith', @jsmith.reload.name
150 150 end
151 151
152 152 def test_lock
153 153 user = User.try_to_login("jsmith", "jsmith")
154 154 assert_equal @jsmith, user
155 155
156 156 @jsmith.status = User::STATUS_LOCKED
157 157 assert @jsmith.save
158 158
159 159 user = User.try_to_login("jsmith", "jsmith")
160 160 assert_equal nil, user
161 161 end
162 162
163 163 if ldap_configured?
164 164 context "#try_to_login using LDAP" do
165 165 context "with failed connection to the LDAP server" do
166 166 should "return nil" do
167 167 @auth_source = AuthSourceLdap.find(1)
168 168 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
169 169
170 170 assert_equal nil, User.try_to_login('edavis', 'wrong')
171 171 end
172 172 end
173 173
174 174 context "with an unsuccessful authentication" do
175 175 should "return nil" do
176 176 assert_equal nil, User.try_to_login('edavis', 'wrong')
177 177 end
178 178 end
179 179
180 180 context "on the fly registration" do
181 181 setup do
182 182 @auth_source = AuthSourceLdap.find(1)
183 183 end
184 184
185 185 context "with a successful authentication" do
186 186 should "create a new user account if it doesn't exist" do
187 187 assert_difference('User.count') do
188 188 user = User.try_to_login('edavis', '123456')
189 189 assert !user.admin?
190 190 end
191 191 end
192 192
193 193 should "retrieve existing user" do
194 194 user = User.try_to_login('edavis', '123456')
195 195 user.admin = true
196 196 user.save!
197 197
198 198 assert_no_difference('User.count') do
199 199 user = User.try_to_login('edavis', '123456')
200 200 assert user.admin?
201 201 end
202 202 end
203 203 end
204 204 end
205 205 end
206 206
207 207 else
208 208 puts "Skipping LDAP tests."
209 209 end
210 210
211 211 def test_create_anonymous
212 212 AnonymousUser.delete_all
213 213 anon = User.anonymous
214 214 assert !anon.new_record?
215 215 assert_kind_of AnonymousUser, anon
216 216 end
217 217
218 218 should_have_one :rss_token
219 219
220 220 def test_rss_key
221 221 assert_nil @jsmith.rss_token
222 222 key = @jsmith.rss_key
223 223 assert_equal 40, key.length
224 224
225 225 @jsmith.reload
226 226 assert_equal key, @jsmith.rss_key
227 227 end
228 228
229 229
230 230 should_have_one :api_token
231 231
232 232 context "User#api_key" do
233 233 should "generate a new one if the user doesn't have one" do
234 234 user = User.generate_with_protected!(:api_token => nil)
235 235 assert_nil user.api_token
236 236
237 237 key = user.api_key
238 238 assert_equal 40, key.length
239 239 user.reload
240 240 assert_equal key, user.api_key
241 241 end
242 242
243 243 should "return the existing api token value" do
244 244 user = User.generate_with_protected!
245 245 token = Token.generate!(:action => 'api')
246 246 user.api_token = token
247 247 assert user.save
248 248
249 249 assert_equal token.value, user.api_key
250 250 end
251 251 end
252 252
253 253 context "User#find_by_api_key" do
254 254 should "return nil if no matching key is found" do
255 255 assert_nil User.find_by_api_key('zzzzzzzzz')
256 256 end
257 257
258 258 should "return nil if the key is found for an inactive user" do
259 259 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
260 260 token = Token.generate!(:action => 'api')
261 261 user.api_token = token
262 262 user.save
263 263
264 264 assert_nil User.find_by_api_key(token.value)
265 265 end
266 266
267 267 should "return the user if the key is found for an active user" do
268 268 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
269 269 token = Token.generate!(:action => 'api')
270 270 user.api_token = token
271 271 user.save
272 272
273 273 assert_equal user, User.find_by_api_key(token.value)
274 274 end
275 275 end
276 276
277 277 def test_roles_for_project
278 278 # user with a role
279 279 roles = @jsmith.roles_for_project(Project.find(1))
280 280 assert_kind_of Role, roles.first
281 281 assert_equal "Manager", roles.first.name
282 282
283 283 # user with no role
284 284 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
285 285 end
286 286
287 287 def test_mail_notification_all
288 @jsmith.mail_notification = true
288 @jsmith.mail_notification = 'all'
289 289 @jsmith.notified_project_ids = []
290 290 @jsmith.save
291 291 @jsmith.reload
292 292 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
293 293 end
294 294
295 295 def test_mail_notification_selected
296 @jsmith.mail_notification = false
296 @jsmith.mail_notification = 'selected'
297 297 @jsmith.notified_project_ids = [1]
298 298 @jsmith.save
299 299 @jsmith.reload
300 300 assert Project.find(1).recipients.include?(@jsmith.mail)
301 301 end
302 302
303 def test_mail_notification_none
304 @jsmith.mail_notification = false
303 def test_mail_notification_only_my_events
304 @jsmith.mail_notification = 'only_my_events'
305 305 @jsmith.notified_project_ids = []
306 306 @jsmith.save
307 307 @jsmith.reload
308 308 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
309 309 end
310 310
311 311 def test_comments_sorting_preference
312 312 assert !@jsmith.wants_comments_in_reverse_order?
313 313 @jsmith.pref.comments_sorting = 'asc'
314 314 assert !@jsmith.wants_comments_in_reverse_order?
315 315 @jsmith.pref.comments_sorting = 'desc'
316 316 assert @jsmith.wants_comments_in_reverse_order?
317 317 end
318 318
319 319 def test_find_by_mail_should_be_case_insensitive
320 320 u = User.find_by_mail('JSmith@somenet.foo')
321 321 assert_not_nil u
322 322 assert_equal 'jsmith@somenet.foo', u.mail
323 323 end
324 324
325 325 def test_random_password
326 326 u = User.new
327 327 u.random_password
328 328 assert !u.password.blank?
329 329 assert !u.password_confirmation.blank?
330 330 end
331 331
332 332 context "#change_password_allowed?" do
333 333 should "be allowed if no auth source is set" do
334 334 user = User.generate_with_protected!
335 335 assert user.change_password_allowed?
336 336 end
337 337
338 338 should "delegate to the auth source" do
339 339 user = User.generate_with_protected!
340 340
341 341 allowed_auth_source = AuthSource.generate!
342 342 def allowed_auth_source.allow_password_changes?; true; end
343 343
344 344 denied_auth_source = AuthSource.generate!
345 345 def denied_auth_source.allow_password_changes?; false; end
346 346
347 347 assert user.change_password_allowed?
348 348
349 349 user.auth_source = allowed_auth_source
350 350 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
351 351
352 352 user.auth_source = denied_auth_source
353 353 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
354 354 end
355 355
356 356 end
357 357
358 358 context "#allowed_to?" do
359 359 context "with a unique project" do
360 360 should "return false if project is archived" do
361 361 project = Project.find(1)
362 362 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
363 363 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
364 364 end
365 365
366 366 should "return false if related module is disabled" do
367 367 project = Project.find(1)
368 368 project.enabled_module_names = ["issue_tracking"]
369 369 assert @admin.allowed_to?(:add_issues, project)
370 370 assert ! @admin.allowed_to?(:view_wiki_pages, project)
371 371 end
372 372
373 373 should "authorize nearly everything for admin users" do
374 374 project = Project.find(1)
375 375 assert ! @admin.member_of?(project)
376 376 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
377 377 assert @admin.allowed_to?(p.to_sym, project)
378 378 end
379 379 end
380 380
381 381 should "authorize normal users depending on their roles" do
382 382 project = Project.find(1)
383 383 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
384 384 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
385 385 end
386 386 end
387 387
388 388 context "with options[:global]" do
389 389 should "authorize if user has at least one role that has this permission" do
390 390 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
391 391 @anonymous = User.find(6)
392 392 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
393 393 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
394 394 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
395 395 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
396 396 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
397 397 end
398 398 end
399 399 end
400 400
401 401 if Object.const_defined?(:OpenID)
402 402
403 403 def test_setting_identity_url
404 404 normalized_open_id_url = 'http://example.com/'
405 405 u = User.new( :identity_url => 'http://example.com/' )
406 406 assert_equal normalized_open_id_url, u.identity_url
407 407 end
408 408
409 409 def test_setting_identity_url_without_trailing_slash
410 410 normalized_open_id_url = 'http://example.com/'
411 411 u = User.new( :identity_url => 'http://example.com' )
412 412 assert_equal normalized_open_id_url, u.identity_url
413 413 end
414 414
415 415 def test_setting_identity_url_without_protocol
416 416 normalized_open_id_url = 'http://example.com/'
417 417 u = User.new( :identity_url => 'example.com' )
418 418 assert_equal normalized_open_id_url, u.identity_url
419 419 end
420 420
421 421 def test_setting_blank_identity_url
422 422 u = User.new( :identity_url => 'example.com' )
423 423 u.identity_url = ''
424 424 assert u.identity_url.blank?
425 425 end
426 426
427 427 def test_setting_invalid_identity_url
428 428 u = User.new( :identity_url => 'this is not an openid url' )
429 429 assert u.identity_url.blank?
430 430 end
431 431
432 432 else
433 433 puts "Skipping openid tests."
434 434 end
435 435
436 436 end
General Comments 0
You need to be logged in to leave comments. Login now