##// END OF EJS Templates
Use \A and \z in validation regexps....
Jean-Philippe Lang -
r10733:147e7a8d6172
parent child
Show More
@@ -1,969 +1,969
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda {|mod|
87 scope :has_module, lambda {|mod|
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 }
89 }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :all_public, lambda { where(:is_public => true) }
92 scope :all_public, lambda { where(:is_public => true) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :allowed_to, lambda {|*args|
94 scope :allowed_to, lambda {|*args|
95 user = User.current
95 user = User.current
96 permission = nil
96 permission = nil
97 if args.first.is_a?(Symbol)
97 if args.first.is_a?(Symbol)
98 permission = args.shift
98 permission = args.shift
99 else
99 else
100 user = args.shift
100 user = args.shift
101 permission = args.shift
101 permission = args.shift
102 end
102 end
103 where(Project.allowed_to_condition(user, permission, *args))
103 where(Project.allowed_to_condition(user, permission, *args))
104 }
104 }
105 scope :like, lambda {|arg|
105 scope :like, lambda {|arg|
106 if arg.blank?
106 if arg.blank?
107 where(nil)
107 where(nil)
108 else
108 else
109 pattern = "%#{arg.to_s.strip.downcase}%"
109 pattern = "%#{arg.to_s.strip.downcase}%"
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 end
111 end
112 }
112 }
113
113
114 def initialize(attributes=nil, *args)
114 def initialize(attributes=nil, *args)
115 super
115 super
116
116
117 initialized = (attributes || {}).stringify_keys
117 initialized = (attributes || {}).stringify_keys
118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 self.identifier = Project.next_identifier
119 self.identifier = Project.next_identifier
120 end
120 end
121 if !initialized.key?('is_public')
121 if !initialized.key?('is_public')
122 self.is_public = Setting.default_projects_public?
122 self.is_public = Setting.default_projects_public?
123 end
123 end
124 if !initialized.key?('enabled_module_names')
124 if !initialized.key?('enabled_module_names')
125 self.enabled_module_names = Setting.default_projects_modules
125 self.enabled_module_names = Setting.default_projects_modules
126 end
126 end
127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 self.trackers = Tracker.sorted.all
128 self.trackers = Tracker.sorted.all
129 end
129 end
130 end
130 end
131
131
132 def identifier=(identifier)
132 def identifier=(identifier)
133 super unless identifier_frozen?
133 super unless identifier_frozen?
134 end
134 end
135
135
136 def identifier_frozen?
136 def identifier_frozen?
137 errors[:identifier].blank? && !(new_record? || identifier.blank?)
137 errors[:identifier].blank? && !(new_record? || identifier.blank?)
138 end
138 end
139
139
140 # returns latest created projects
140 # returns latest created projects
141 # non public projects will be returned only if user is a member of those
141 # non public projects will be returned only if user is a member of those
142 def self.latest(user=nil, count=5)
142 def self.latest(user=nil, count=5)
143 visible(user).limit(count).order("created_on DESC").all
143 visible(user).limit(count).order("created_on DESC").all
144 end
144 end
145
145
146 # Returns true if the project is visible to +user+ or to the current user.
146 # Returns true if the project is visible to +user+ or to the current user.
147 def visible?(user=User.current)
147 def visible?(user=User.current)
148 user.allowed_to?(:view_project, self)
148 user.allowed_to?(:view_project, self)
149 end
149 end
150
150
151 # Returns a SQL conditions string used to find all projects visible by the specified user.
151 # Returns a SQL conditions string used to find all projects visible by the specified user.
152 #
152 #
153 # Examples:
153 # Examples:
154 # Project.visible_condition(admin) => "projects.status = 1"
154 # Project.visible_condition(admin) => "projects.status = 1"
155 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
155 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
156 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
156 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
157 def self.visible_condition(user, options={})
157 def self.visible_condition(user, options={})
158 allowed_to_condition(user, :view_project, options)
158 allowed_to_condition(user, :view_project, options)
159 end
159 end
160
160
161 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
161 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
162 #
162 #
163 # Valid options:
163 # Valid options:
164 # * :project => limit the condition to project
164 # * :project => limit the condition to project
165 # * :with_subprojects => limit the condition to project and its subprojects
165 # * :with_subprojects => limit the condition to project and its subprojects
166 # * :member => limit the condition to the user projects
166 # * :member => limit the condition to the user projects
167 def self.allowed_to_condition(user, permission, options={})
167 def self.allowed_to_condition(user, permission, options={})
168 perm = Redmine::AccessControl.permission(permission)
168 perm = Redmine::AccessControl.permission(permission)
169 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
169 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
170 if perm && perm.project_module
170 if perm && perm.project_module
171 # If the permission belongs to a project module, make sure the module is enabled
171 # If the permission belongs to a project module, make sure the module is enabled
172 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
172 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
173 end
173 end
174 if options[:project]
174 if options[:project]
175 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
175 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
176 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
176 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
177 base_statement = "(#{project_statement}) AND (#{base_statement})"
177 base_statement = "(#{project_statement}) AND (#{base_statement})"
178 end
178 end
179
179
180 if user.admin?
180 if user.admin?
181 base_statement
181 base_statement
182 else
182 else
183 statement_by_role = {}
183 statement_by_role = {}
184 unless options[:member]
184 unless options[:member]
185 role = user.logged? ? Role.non_member : Role.anonymous
185 role = user.logged? ? Role.non_member : Role.anonymous
186 if role.allowed_to?(permission)
186 if role.allowed_to?(permission)
187 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
187 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
188 end
188 end
189 end
189 end
190 if user.logged?
190 if user.logged?
191 user.projects_by_role.each do |role, projects|
191 user.projects_by_role.each do |role, projects|
192 if role.allowed_to?(permission) && projects.any?
192 if role.allowed_to?(permission) && projects.any?
193 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
193 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
194 end
194 end
195 end
195 end
196 end
196 end
197 if statement_by_role.empty?
197 if statement_by_role.empty?
198 "1=0"
198 "1=0"
199 else
199 else
200 if block_given?
200 if block_given?
201 statement_by_role.each do |role, statement|
201 statement_by_role.each do |role, statement|
202 if s = yield(role, user)
202 if s = yield(role, user)
203 statement_by_role[role] = "(#{statement} AND (#{s}))"
203 statement_by_role[role] = "(#{statement} AND (#{s}))"
204 end
204 end
205 end
205 end
206 end
206 end
207 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
207 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
208 end
208 end
209 end
209 end
210 end
210 end
211
211
212 # Returns the Systemwide and project specific activities
212 # Returns the Systemwide and project specific activities
213 def activities(include_inactive=false)
213 def activities(include_inactive=false)
214 if include_inactive
214 if include_inactive
215 return all_activities
215 return all_activities
216 else
216 else
217 return active_activities
217 return active_activities
218 end
218 end
219 end
219 end
220
220
221 # Will create a new Project specific Activity or update an existing one
221 # Will create a new Project specific Activity or update an existing one
222 #
222 #
223 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
223 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
224 # does not successfully save.
224 # does not successfully save.
225 def update_or_create_time_entry_activity(id, activity_hash)
225 def update_or_create_time_entry_activity(id, activity_hash)
226 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
226 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
227 self.create_time_entry_activity_if_needed(activity_hash)
227 self.create_time_entry_activity_if_needed(activity_hash)
228 else
228 else
229 activity = project.time_entry_activities.find_by_id(id.to_i)
229 activity = project.time_entry_activities.find_by_id(id.to_i)
230 activity.update_attributes(activity_hash) if activity
230 activity.update_attributes(activity_hash) if activity
231 end
231 end
232 end
232 end
233
233
234 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
234 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
235 #
235 #
236 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
237 # does not successfully save.
237 # does not successfully save.
238 def create_time_entry_activity_if_needed(activity)
238 def create_time_entry_activity_if_needed(activity)
239 if activity['parent_id']
239 if activity['parent_id']
240
240
241 parent_activity = TimeEntryActivity.find(activity['parent_id'])
241 parent_activity = TimeEntryActivity.find(activity['parent_id'])
242 activity['name'] = parent_activity.name
242 activity['name'] = parent_activity.name
243 activity['position'] = parent_activity.position
243 activity['position'] = parent_activity.position
244
244
245 if Enumeration.overridding_change?(activity, parent_activity)
245 if Enumeration.overridding_change?(activity, parent_activity)
246 project_activity = self.time_entry_activities.create(activity)
246 project_activity = self.time_entry_activities.create(activity)
247
247
248 if project_activity.new_record?
248 if project_activity.new_record?
249 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
249 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
250 else
250 else
251 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
251 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
252 end
252 end
253 end
253 end
254 end
254 end
255 end
255 end
256
256
257 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
257 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
258 #
258 #
259 # Examples:
259 # Examples:
260 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
260 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
261 # project.project_condition(false) => "projects.id = 1"
261 # project.project_condition(false) => "projects.id = 1"
262 def project_condition(with_subprojects)
262 def project_condition(with_subprojects)
263 cond = "#{Project.table_name}.id = #{id}"
263 cond = "#{Project.table_name}.id = #{id}"
264 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
264 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
265 cond
265 cond
266 end
266 end
267
267
268 def self.find(*args)
268 def self.find(*args)
269 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
269 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
270 project = find_by_identifier(*args)
270 project = find_by_identifier(*args)
271 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
271 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
272 project
272 project
273 else
273 else
274 super
274 super
275 end
275 end
276 end
276 end
277
277
278 def self.find_by_param(*args)
278 def self.find_by_param(*args)
279 self.find(*args)
279 self.find(*args)
280 end
280 end
281
281
282 def reload(*args)
282 def reload(*args)
283 @shared_versions = nil
283 @shared_versions = nil
284 @rolled_up_versions = nil
284 @rolled_up_versions = nil
285 @rolled_up_trackers = nil
285 @rolled_up_trackers = nil
286 @all_issue_custom_fields = nil
286 @all_issue_custom_fields = nil
287 @all_time_entry_custom_fields = nil
287 @all_time_entry_custom_fields = nil
288 @to_param = nil
288 @to_param = nil
289 @allowed_parents = nil
289 @allowed_parents = nil
290 @allowed_permissions = nil
290 @allowed_permissions = nil
291 @actions_allowed = nil
291 @actions_allowed = nil
292 super
292 super
293 end
293 end
294
294
295 def to_param
295 def to_param
296 # id is used for projects with a numeric identifier (compatibility)
296 # id is used for projects with a numeric identifier (compatibility)
297 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
297 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
298 end
298 end
299
299
300 def active?
300 def active?
301 self.status == STATUS_ACTIVE
301 self.status == STATUS_ACTIVE
302 end
302 end
303
303
304 def archived?
304 def archived?
305 self.status == STATUS_ARCHIVED
305 self.status == STATUS_ARCHIVED
306 end
306 end
307
307
308 # Archives the project and its descendants
308 # Archives the project and its descendants
309 def archive
309 def archive
310 # Check that there is no issue of a non descendant project that is assigned
310 # Check that there is no issue of a non descendant project that is assigned
311 # to one of the project or descendant versions
311 # to one of the project or descendant versions
312 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
312 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
313 if v_ids.any? &&
313 if v_ids.any? &&
314 Issue.
314 Issue.
315 includes(:project).
315 includes(:project).
316 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
316 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
317 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
317 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
318 exists?
318 exists?
319 return false
319 return false
320 end
320 end
321 Project.transaction do
321 Project.transaction do
322 archive!
322 archive!
323 end
323 end
324 true
324 true
325 end
325 end
326
326
327 # Unarchives the project
327 # Unarchives the project
328 # All its ancestors must be active
328 # All its ancestors must be active
329 def unarchive
329 def unarchive
330 return false if ancestors.detect {|a| !a.active?}
330 return false if ancestors.detect {|a| !a.active?}
331 update_attribute :status, STATUS_ACTIVE
331 update_attribute :status, STATUS_ACTIVE
332 end
332 end
333
333
334 def close
334 def close
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
336 end
336 end
337
337
338 def reopen
338 def reopen
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
340 end
340 end
341
341
342 # Returns an array of projects the project can be moved to
342 # Returns an array of projects the project can be moved to
343 # by the current user
343 # by the current user
344 def allowed_parents
344 def allowed_parents
345 return @allowed_parents if @allowed_parents
345 return @allowed_parents if @allowed_parents
346 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
346 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
347 @allowed_parents = @allowed_parents - self_and_descendants
347 @allowed_parents = @allowed_parents - self_and_descendants
348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
349 @allowed_parents << nil
349 @allowed_parents << nil
350 end
350 end
351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
352 @allowed_parents << parent
352 @allowed_parents << parent
353 end
353 end
354 @allowed_parents
354 @allowed_parents
355 end
355 end
356
356
357 # Sets the parent of the project with authorization check
357 # Sets the parent of the project with authorization check
358 def set_allowed_parent!(p)
358 def set_allowed_parent!(p)
359 unless p.nil? || p.is_a?(Project)
359 unless p.nil? || p.is_a?(Project)
360 if p.to_s.blank?
360 if p.to_s.blank?
361 p = nil
361 p = nil
362 else
362 else
363 p = Project.find_by_id(p)
363 p = Project.find_by_id(p)
364 return false unless p
364 return false unless p
365 end
365 end
366 end
366 end
367 if p.nil?
367 if p.nil?
368 if !new_record? && allowed_parents.empty?
368 if !new_record? && allowed_parents.empty?
369 return false
369 return false
370 end
370 end
371 elsif !allowed_parents.include?(p)
371 elsif !allowed_parents.include?(p)
372 return false
372 return false
373 end
373 end
374 set_parent!(p)
374 set_parent!(p)
375 end
375 end
376
376
377 # Sets the parent of the project
377 # Sets the parent of the project
378 # Argument can be either a Project, a String, a Fixnum or nil
378 # Argument can be either a Project, a String, a Fixnum or nil
379 def set_parent!(p)
379 def set_parent!(p)
380 unless p.nil? || p.is_a?(Project)
380 unless p.nil? || p.is_a?(Project)
381 if p.to_s.blank?
381 if p.to_s.blank?
382 p = nil
382 p = nil
383 else
383 else
384 p = Project.find_by_id(p)
384 p = Project.find_by_id(p)
385 return false unless p
385 return false unless p
386 end
386 end
387 end
387 end
388 if p == parent && !p.nil?
388 if p == parent && !p.nil?
389 # Nothing to do
389 # Nothing to do
390 true
390 true
391 elsif p.nil? || (p.active? && move_possible?(p))
391 elsif p.nil? || (p.active? && move_possible?(p))
392 set_or_update_position_under(p)
392 set_or_update_position_under(p)
393 Issue.update_versions_from_hierarchy_change(self)
393 Issue.update_versions_from_hierarchy_change(self)
394 true
394 true
395 else
395 else
396 # Can not move to the given target
396 # Can not move to the given target
397 false
397 false
398 end
398 end
399 end
399 end
400
400
401 # Recalculates all lft and rgt values based on project names
401 # Recalculates all lft and rgt values based on project names
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
403 # Used in BuildProjectsTree migration
403 # Used in BuildProjectsTree migration
404 def self.rebuild_tree!
404 def self.rebuild_tree!
405 transaction do
405 transaction do
406 update_all "lft = NULL, rgt = NULL"
406 update_all "lft = NULL, rgt = NULL"
407 rebuild!(false)
407 rebuild!(false)
408 end
408 end
409 end
409 end
410
410
411 # Returns an array of the trackers used by the project and its active sub projects
411 # Returns an array of the trackers used by the project and its active sub projects
412 def rolled_up_trackers
412 def rolled_up_trackers
413 @rolled_up_trackers ||=
413 @rolled_up_trackers ||=
414 Tracker.
414 Tracker.
415 joins(:projects).
415 joins(:projects).
416 select("DISTINCT #{Tracker.table_name}.*").
416 select("DISTINCT #{Tracker.table_name}.*").
417 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
417 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
418 sorted.
418 sorted.
419 all
419 all
420 end
420 end
421
421
422 # Closes open and locked project versions that are completed
422 # Closes open and locked project versions that are completed
423 def close_completed_versions
423 def close_completed_versions
424 Version.transaction do
424 Version.transaction do
425 versions.where(:status => %w(open locked)).all.each do |version|
425 versions.where(:status => %w(open locked)).all.each do |version|
426 if version.completed?
426 if version.completed?
427 version.update_attribute(:status, 'closed')
427 version.update_attribute(:status, 'closed')
428 end
428 end
429 end
429 end
430 end
430 end
431 end
431 end
432
432
433 # Returns a scope of the Versions on subprojects
433 # Returns a scope of the Versions on subprojects
434 def rolled_up_versions
434 def rolled_up_versions
435 @rolled_up_versions ||=
435 @rolled_up_versions ||=
436 Version.scoped(:include => :project,
436 Version.scoped(:include => :project,
437 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
437 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
438 end
438 end
439
439
440 # Returns a scope of the Versions used by the project
440 # Returns a scope of the Versions used by the project
441 def shared_versions
441 def shared_versions
442 if new_record?
442 if new_record?
443 Version.scoped(:include => :project,
443 Version.scoped(:include => :project,
444 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
444 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
445 else
445 else
446 @shared_versions ||= begin
446 @shared_versions ||= begin
447 r = root? ? self : root
447 r = root? ? self : root
448 Version.scoped(:include => :project,
448 Version.scoped(:include => :project,
449 :conditions => "#{Project.table_name}.id = #{id}" +
449 :conditions => "#{Project.table_name}.id = #{id}" +
450 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
450 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
451 " #{Version.table_name}.sharing = 'system'" +
451 " #{Version.table_name}.sharing = 'system'" +
452 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
452 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
453 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
453 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
454 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
454 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
455 "))")
455 "))")
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 # Returns a hash of project users grouped by role
460 # Returns a hash of project users grouped by role
461 def users_by_role
461 def users_by_role
462 members.includes(:user, :roles).all.inject({}) do |h, m|
462 members.includes(:user, :roles).all.inject({}) do |h, m|
463 m.roles.each do |r|
463 m.roles.each do |r|
464 h[r] ||= []
464 h[r] ||= []
465 h[r] << m.user
465 h[r] << m.user
466 end
466 end
467 h
467 h
468 end
468 end
469 end
469 end
470
470
471 # Deletes all project's members
471 # Deletes all project's members
472 def delete_all_members
472 def delete_all_members
473 me, mr = Member.table_name, MemberRole.table_name
473 me, mr = Member.table_name, MemberRole.table_name
474 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
474 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
475 Member.delete_all(['project_id = ?', id])
475 Member.delete_all(['project_id = ?', id])
476 end
476 end
477
477
478 # Users/groups issues can be assigned to
478 # Users/groups issues can be assigned to
479 def assignable_users
479 def assignable_users
480 assignable = Setting.issue_group_assignment? ? member_principals : members
480 assignable = Setting.issue_group_assignment? ? member_principals : members
481 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
481 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
482 end
482 end
483
483
484 # Returns the mail adresses of users that should be always notified on project events
484 # Returns the mail adresses of users that should be always notified on project events
485 def recipients
485 def recipients
486 notified_users.collect {|user| user.mail}
486 notified_users.collect {|user| user.mail}
487 end
487 end
488
488
489 # Returns the users that should be notified on project events
489 # Returns the users that should be notified on project events
490 def notified_users
490 def notified_users
491 # TODO: User part should be extracted to User#notify_about?
491 # TODO: User part should be extracted to User#notify_about?
492 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
492 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
493 end
493 end
494
494
495 # Returns an array of all custom fields enabled for project issues
495 # Returns an array of all custom fields enabled for project issues
496 # (explictly associated custom fields and custom fields enabled for all projects)
496 # (explictly associated custom fields and custom fields enabled for all projects)
497 def all_issue_custom_fields
497 def all_issue_custom_fields
498 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
498 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
499 end
499 end
500
500
501 # Returns an array of all custom fields enabled for project time entries
501 # Returns an array of all custom fields enabled for project time entries
502 # (explictly associated custom fields and custom fields enabled for all projects)
502 # (explictly associated custom fields and custom fields enabled for all projects)
503 def all_time_entry_custom_fields
503 def all_time_entry_custom_fields
504 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
504 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
505 end
505 end
506
506
507 def project
507 def project
508 self
508 self
509 end
509 end
510
510
511 def <=>(project)
511 def <=>(project)
512 name.downcase <=> project.name.downcase
512 name.downcase <=> project.name.downcase
513 end
513 end
514
514
515 def to_s
515 def to_s
516 name
516 name
517 end
517 end
518
518
519 # Returns a short description of the projects (first lines)
519 # Returns a short description of the projects (first lines)
520 def short_description(length = 255)
520 def short_description(length = 255)
521 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
521 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
522 end
522 end
523
523
524 def css_classes
524 def css_classes
525 s = 'project'
525 s = 'project'
526 s << ' root' if root?
526 s << ' root' if root?
527 s << ' child' if child?
527 s << ' child' if child?
528 s << (leaf? ? ' leaf' : ' parent')
528 s << (leaf? ? ' leaf' : ' parent')
529 unless active?
529 unless active?
530 if archived?
530 if archived?
531 s << ' archived'
531 s << ' archived'
532 else
532 else
533 s << ' closed'
533 s << ' closed'
534 end
534 end
535 end
535 end
536 s
536 s
537 end
537 end
538
538
539 # The earliest start date of a project, based on it's issues and versions
539 # The earliest start date of a project, based on it's issues and versions
540 def start_date
540 def start_date
541 [
541 [
542 issues.minimum('start_date'),
542 issues.minimum('start_date'),
543 shared_versions.collect(&:effective_date),
543 shared_versions.collect(&:effective_date),
544 shared_versions.collect(&:start_date)
544 shared_versions.collect(&:start_date)
545 ].flatten.compact.min
545 ].flatten.compact.min
546 end
546 end
547
547
548 # The latest due date of an issue or version
548 # The latest due date of an issue or version
549 def due_date
549 def due_date
550 [
550 [
551 issues.maximum('due_date'),
551 issues.maximum('due_date'),
552 shared_versions.collect(&:effective_date),
552 shared_versions.collect(&:effective_date),
553 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
553 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
554 ].flatten.compact.max
554 ].flatten.compact.max
555 end
555 end
556
556
557 def overdue?
557 def overdue?
558 active? && !due_date.nil? && (due_date < Date.today)
558 active? && !due_date.nil? && (due_date < Date.today)
559 end
559 end
560
560
561 # Returns the percent completed for this project, based on the
561 # Returns the percent completed for this project, based on the
562 # progress on it's versions.
562 # progress on it's versions.
563 def completed_percent(options={:include_subprojects => false})
563 def completed_percent(options={:include_subprojects => false})
564 if options.delete(:include_subprojects)
564 if options.delete(:include_subprojects)
565 total = self_and_descendants.collect(&:completed_percent).sum
565 total = self_and_descendants.collect(&:completed_percent).sum
566
566
567 total / self_and_descendants.count
567 total / self_and_descendants.count
568 else
568 else
569 if versions.count > 0
569 if versions.count > 0
570 total = versions.collect(&:completed_pourcent).sum
570 total = versions.collect(&:completed_pourcent).sum
571
571
572 total / versions.count
572 total / versions.count
573 else
573 else
574 100
574 100
575 end
575 end
576 end
576 end
577 end
577 end
578
578
579 # Return true if this project allows to do the specified action.
579 # Return true if this project allows to do the specified action.
580 # action can be:
580 # action can be:
581 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
581 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
582 # * a permission Symbol (eg. :edit_project)
582 # * a permission Symbol (eg. :edit_project)
583 def allows_to?(action)
583 def allows_to?(action)
584 if archived?
584 if archived?
585 # No action allowed on archived projects
585 # No action allowed on archived projects
586 return false
586 return false
587 end
587 end
588 unless active? || Redmine::AccessControl.read_action?(action)
588 unless active? || Redmine::AccessControl.read_action?(action)
589 # No write action allowed on closed projects
589 # No write action allowed on closed projects
590 return false
590 return false
591 end
591 end
592 # No action allowed on disabled modules
592 # No action allowed on disabled modules
593 if action.is_a? Hash
593 if action.is_a? Hash
594 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
594 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
595 else
595 else
596 allowed_permissions.include? action
596 allowed_permissions.include? action
597 end
597 end
598 end
598 end
599
599
600 def module_enabled?(module_name)
600 def module_enabled?(module_name)
601 module_name = module_name.to_s
601 module_name = module_name.to_s
602 enabled_modules.detect {|m| m.name == module_name}
602 enabled_modules.detect {|m| m.name == module_name}
603 end
603 end
604
604
605 def enabled_module_names=(module_names)
605 def enabled_module_names=(module_names)
606 if module_names && module_names.is_a?(Array)
606 if module_names && module_names.is_a?(Array)
607 module_names = module_names.collect(&:to_s).reject(&:blank?)
607 module_names = module_names.collect(&:to_s).reject(&:blank?)
608 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
608 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
609 else
609 else
610 enabled_modules.clear
610 enabled_modules.clear
611 end
611 end
612 end
612 end
613
613
614 # Returns an array of the enabled modules names
614 # Returns an array of the enabled modules names
615 def enabled_module_names
615 def enabled_module_names
616 enabled_modules.collect(&:name)
616 enabled_modules.collect(&:name)
617 end
617 end
618
618
619 # Enable a specific module
619 # Enable a specific module
620 #
620 #
621 # Examples:
621 # Examples:
622 # project.enable_module!(:issue_tracking)
622 # project.enable_module!(:issue_tracking)
623 # project.enable_module!("issue_tracking")
623 # project.enable_module!("issue_tracking")
624 def enable_module!(name)
624 def enable_module!(name)
625 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
625 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
626 end
626 end
627
627
628 # Disable a module if it exists
628 # Disable a module if it exists
629 #
629 #
630 # Examples:
630 # Examples:
631 # project.disable_module!(:issue_tracking)
631 # project.disable_module!(:issue_tracking)
632 # project.disable_module!("issue_tracking")
632 # project.disable_module!("issue_tracking")
633 # project.disable_module!(project.enabled_modules.first)
633 # project.disable_module!(project.enabled_modules.first)
634 def disable_module!(target)
634 def disable_module!(target)
635 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
635 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
636 target.destroy unless target.blank?
636 target.destroy unless target.blank?
637 end
637 end
638
638
639 safe_attributes 'name',
639 safe_attributes 'name',
640 'description',
640 'description',
641 'homepage',
641 'homepage',
642 'is_public',
642 'is_public',
643 'identifier',
643 'identifier',
644 'custom_field_values',
644 'custom_field_values',
645 'custom_fields',
645 'custom_fields',
646 'tracker_ids',
646 'tracker_ids',
647 'issue_custom_field_ids'
647 'issue_custom_field_ids'
648
648
649 safe_attributes 'enabled_module_names',
649 safe_attributes 'enabled_module_names',
650 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
650 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
651
651
652 # Returns an array of projects that are in this project's hierarchy
652 # Returns an array of projects that are in this project's hierarchy
653 #
653 #
654 # Example: parents, children, siblings
654 # Example: parents, children, siblings
655 def hierarchy
655 def hierarchy
656 parents = project.self_and_ancestors || []
656 parents = project.self_and_ancestors || []
657 descendants = project.descendants || []
657 descendants = project.descendants || []
658 project_hierarchy = parents | descendants # Set union
658 project_hierarchy = parents | descendants # Set union
659 end
659 end
660
660
661 # Returns an auto-generated project identifier based on the last identifier used
661 # Returns an auto-generated project identifier based on the last identifier used
662 def self.next_identifier
662 def self.next_identifier
663 p = Project.order('created_on DESC').first
663 p = Project.order('created_on DESC').first
664 p.nil? ? nil : p.identifier.to_s.succ
664 p.nil? ? nil : p.identifier.to_s.succ
665 end
665 end
666
666
667 # Copies and saves the Project instance based on the +project+.
667 # Copies and saves the Project instance based on the +project+.
668 # Duplicates the source project's:
668 # Duplicates the source project's:
669 # * Wiki
669 # * Wiki
670 # * Versions
670 # * Versions
671 # * Categories
671 # * Categories
672 # * Issues
672 # * Issues
673 # * Members
673 # * Members
674 # * Queries
674 # * Queries
675 #
675 #
676 # Accepts an +options+ argument to specify what to copy
676 # Accepts an +options+ argument to specify what to copy
677 #
677 #
678 # Examples:
678 # Examples:
679 # project.copy(1) # => copies everything
679 # project.copy(1) # => copies everything
680 # project.copy(1, :only => 'members') # => copies members only
680 # project.copy(1, :only => 'members') # => copies members only
681 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
681 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
682 def copy(project, options={})
682 def copy(project, options={})
683 project = project.is_a?(Project) ? project : Project.find(project)
683 project = project.is_a?(Project) ? project : Project.find(project)
684
684
685 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
685 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
686 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
686 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
687
687
688 Project.transaction do
688 Project.transaction do
689 if save
689 if save
690 reload
690 reload
691 to_be_copied.each do |name|
691 to_be_copied.each do |name|
692 send "copy_#{name}", project
692 send "copy_#{name}", project
693 end
693 end
694 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
694 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
695 save
695 save
696 end
696 end
697 end
697 end
698 end
698 end
699
699
700 # Returns a new unsaved Project instance with attributes copied from +project+
700 # Returns a new unsaved Project instance with attributes copied from +project+
701 def self.copy_from(project)
701 def self.copy_from(project)
702 project = project.is_a?(Project) ? project : Project.find(project)
702 project = project.is_a?(Project) ? project : Project.find(project)
703 # clear unique attributes
703 # clear unique attributes
704 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
704 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
705 copy = Project.new(attributes)
705 copy = Project.new(attributes)
706 copy.enabled_modules = project.enabled_modules
706 copy.enabled_modules = project.enabled_modules
707 copy.trackers = project.trackers
707 copy.trackers = project.trackers
708 copy.custom_values = project.custom_values.collect {|v| v.clone}
708 copy.custom_values = project.custom_values.collect {|v| v.clone}
709 copy.issue_custom_fields = project.issue_custom_fields
709 copy.issue_custom_fields = project.issue_custom_fields
710 copy
710 copy
711 end
711 end
712
712
713 # Yields the given block for each project with its level in the tree
713 # Yields the given block for each project with its level in the tree
714 def self.project_tree(projects, &block)
714 def self.project_tree(projects, &block)
715 ancestors = []
715 ancestors = []
716 projects.sort_by(&:lft).each do |project|
716 projects.sort_by(&:lft).each do |project|
717 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
717 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
718 ancestors.pop
718 ancestors.pop
719 end
719 end
720 yield project, ancestors.size
720 yield project, ancestors.size
721 ancestors << project
721 ancestors << project
722 end
722 end
723 end
723 end
724
724
725 private
725 private
726
726
727 # Copies wiki from +project+
727 # Copies wiki from +project+
728 def copy_wiki(project)
728 def copy_wiki(project)
729 # Check that the source project has a wiki first
729 # Check that the source project has a wiki first
730 unless project.wiki.nil?
730 unless project.wiki.nil?
731 self.wiki ||= Wiki.new
731 self.wiki ||= Wiki.new
732 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
732 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
733 wiki_pages_map = {}
733 wiki_pages_map = {}
734 project.wiki.pages.each do |page|
734 project.wiki.pages.each do |page|
735 # Skip pages without content
735 # Skip pages without content
736 next if page.content.nil?
736 next if page.content.nil?
737 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
737 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
738 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
738 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
739 new_wiki_page.content = new_wiki_content
739 new_wiki_page.content = new_wiki_content
740 wiki.pages << new_wiki_page
740 wiki.pages << new_wiki_page
741 wiki_pages_map[page.id] = new_wiki_page
741 wiki_pages_map[page.id] = new_wiki_page
742 end
742 end
743 wiki.save
743 wiki.save
744 # Reproduce page hierarchy
744 # Reproduce page hierarchy
745 project.wiki.pages.each do |page|
745 project.wiki.pages.each do |page|
746 if page.parent_id && wiki_pages_map[page.id]
746 if page.parent_id && wiki_pages_map[page.id]
747 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
747 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
748 wiki_pages_map[page.id].save
748 wiki_pages_map[page.id].save
749 end
749 end
750 end
750 end
751 end
751 end
752 end
752 end
753
753
754 # Copies versions from +project+
754 # Copies versions from +project+
755 def copy_versions(project)
755 def copy_versions(project)
756 project.versions.each do |version|
756 project.versions.each do |version|
757 new_version = Version.new
757 new_version = Version.new
758 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
758 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
759 self.versions << new_version
759 self.versions << new_version
760 end
760 end
761 end
761 end
762
762
763 # Copies issue categories from +project+
763 # Copies issue categories from +project+
764 def copy_issue_categories(project)
764 def copy_issue_categories(project)
765 project.issue_categories.each do |issue_category|
765 project.issue_categories.each do |issue_category|
766 new_issue_category = IssueCategory.new
766 new_issue_category = IssueCategory.new
767 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
767 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
768 self.issue_categories << new_issue_category
768 self.issue_categories << new_issue_category
769 end
769 end
770 end
770 end
771
771
772 # Copies issues from +project+
772 # Copies issues from +project+
773 def copy_issues(project)
773 def copy_issues(project)
774 # Stores the source issue id as a key and the copied issues as the
774 # Stores the source issue id as a key and the copied issues as the
775 # value. Used to map the two togeather for issue relations.
775 # value. Used to map the two togeather for issue relations.
776 issues_map = {}
776 issues_map = {}
777
777
778 # Store status and reopen locked/closed versions
778 # Store status and reopen locked/closed versions
779 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
779 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
780 version_statuses.each do |version, status|
780 version_statuses.each do |version, status|
781 version.update_attribute :status, 'open'
781 version.update_attribute :status, 'open'
782 end
782 end
783
783
784 # Get issues sorted by root_id, lft so that parent issues
784 # Get issues sorted by root_id, lft so that parent issues
785 # get copied before their children
785 # get copied before their children
786 project.issues.reorder('root_id, lft').all.each do |issue|
786 project.issues.reorder('root_id, lft').all.each do |issue|
787 new_issue = Issue.new
787 new_issue = Issue.new
788 new_issue.copy_from(issue, :subtasks => false, :link => false)
788 new_issue.copy_from(issue, :subtasks => false, :link => false)
789 new_issue.project = self
789 new_issue.project = self
790 # Reassign fixed_versions by name, since names are unique per project
790 # Reassign fixed_versions by name, since names are unique per project
791 if issue.fixed_version && issue.fixed_version.project == project
791 if issue.fixed_version && issue.fixed_version.project == project
792 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
792 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
793 end
793 end
794 # Reassign the category by name, since names are unique per project
794 # Reassign the category by name, since names are unique per project
795 if issue.category
795 if issue.category
796 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
796 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
797 end
797 end
798 # Parent issue
798 # Parent issue
799 if issue.parent_id
799 if issue.parent_id
800 if copied_parent = issues_map[issue.parent_id]
800 if copied_parent = issues_map[issue.parent_id]
801 new_issue.parent_issue_id = copied_parent.id
801 new_issue.parent_issue_id = copied_parent.id
802 end
802 end
803 end
803 end
804
804
805 self.issues << new_issue
805 self.issues << new_issue
806 if new_issue.new_record?
806 if new_issue.new_record?
807 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
807 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
808 else
808 else
809 issues_map[issue.id] = new_issue unless new_issue.new_record?
809 issues_map[issue.id] = new_issue unless new_issue.new_record?
810 end
810 end
811 end
811 end
812
812
813 # Restore locked/closed version statuses
813 # Restore locked/closed version statuses
814 version_statuses.each do |version, status|
814 version_statuses.each do |version, status|
815 version.update_attribute :status, status
815 version.update_attribute :status, status
816 end
816 end
817
817
818 # Relations after in case issues related each other
818 # Relations after in case issues related each other
819 project.issues.each do |issue|
819 project.issues.each do |issue|
820 new_issue = issues_map[issue.id]
820 new_issue = issues_map[issue.id]
821 unless new_issue
821 unless new_issue
822 # Issue was not copied
822 # Issue was not copied
823 next
823 next
824 end
824 end
825
825
826 # Relations
826 # Relations
827 issue.relations_from.each do |source_relation|
827 issue.relations_from.each do |source_relation|
828 new_issue_relation = IssueRelation.new
828 new_issue_relation = IssueRelation.new
829 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
829 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
830 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
830 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
831 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
831 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
832 new_issue_relation.issue_to = source_relation.issue_to
832 new_issue_relation.issue_to = source_relation.issue_to
833 end
833 end
834 new_issue.relations_from << new_issue_relation
834 new_issue.relations_from << new_issue_relation
835 end
835 end
836
836
837 issue.relations_to.each do |source_relation|
837 issue.relations_to.each do |source_relation|
838 new_issue_relation = IssueRelation.new
838 new_issue_relation = IssueRelation.new
839 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
839 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
840 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
840 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
841 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
841 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
842 new_issue_relation.issue_from = source_relation.issue_from
842 new_issue_relation.issue_from = source_relation.issue_from
843 end
843 end
844 new_issue.relations_to << new_issue_relation
844 new_issue.relations_to << new_issue_relation
845 end
845 end
846 end
846 end
847 end
847 end
848
848
849 # Copies members from +project+
849 # Copies members from +project+
850 def copy_members(project)
850 def copy_members(project)
851 # Copy users first, then groups to handle members with inherited and given roles
851 # Copy users first, then groups to handle members with inherited and given roles
852 members_to_copy = []
852 members_to_copy = []
853 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
853 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
854 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
854 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
855
855
856 members_to_copy.each do |member|
856 members_to_copy.each do |member|
857 new_member = Member.new
857 new_member = Member.new
858 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
858 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
859 # only copy non inherited roles
859 # only copy non inherited roles
860 # inherited roles will be added when copying the group membership
860 # inherited roles will be added when copying the group membership
861 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
861 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
862 next if role_ids.empty?
862 next if role_ids.empty?
863 new_member.role_ids = role_ids
863 new_member.role_ids = role_ids
864 new_member.project = self
864 new_member.project = self
865 self.members << new_member
865 self.members << new_member
866 end
866 end
867 end
867 end
868
868
869 # Copies queries from +project+
869 # Copies queries from +project+
870 def copy_queries(project)
870 def copy_queries(project)
871 project.queries.each do |query|
871 project.queries.each do |query|
872 new_query = ::Query.new
872 new_query = ::Query.new
873 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
873 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
874 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
874 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
875 new_query.project = self
875 new_query.project = self
876 new_query.user_id = query.user_id
876 new_query.user_id = query.user_id
877 self.queries << new_query
877 self.queries << new_query
878 end
878 end
879 end
879 end
880
880
881 # Copies boards from +project+
881 # Copies boards from +project+
882 def copy_boards(project)
882 def copy_boards(project)
883 project.boards.each do |board|
883 project.boards.each do |board|
884 new_board = Board.new
884 new_board = Board.new
885 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
885 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
886 new_board.project = self
886 new_board.project = self
887 self.boards << new_board
887 self.boards << new_board
888 end
888 end
889 end
889 end
890
890
891 def allowed_permissions
891 def allowed_permissions
892 @allowed_permissions ||= begin
892 @allowed_permissions ||= begin
893 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
893 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
894 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
894 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
895 end
895 end
896 end
896 end
897
897
898 def allowed_actions
898 def allowed_actions
899 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
899 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
900 end
900 end
901
901
902 # Returns all the active Systemwide and project specific activities
902 # Returns all the active Systemwide and project specific activities
903 def active_activities
903 def active_activities
904 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
904 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
905
905
906 if overridden_activity_ids.empty?
906 if overridden_activity_ids.empty?
907 return TimeEntryActivity.shared.active
907 return TimeEntryActivity.shared.active
908 else
908 else
909 return system_activities_and_project_overrides
909 return system_activities_and_project_overrides
910 end
910 end
911 end
911 end
912
912
913 # Returns all the Systemwide and project specific activities
913 # Returns all the Systemwide and project specific activities
914 # (inactive and active)
914 # (inactive and active)
915 def all_activities
915 def all_activities
916 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
916 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
917
917
918 if overridden_activity_ids.empty?
918 if overridden_activity_ids.empty?
919 return TimeEntryActivity.shared
919 return TimeEntryActivity.shared
920 else
920 else
921 return system_activities_and_project_overrides(true)
921 return system_activities_and_project_overrides(true)
922 end
922 end
923 end
923 end
924
924
925 # Returns the systemwide active activities merged with the project specific overrides
925 # Returns the systemwide active activities merged with the project specific overrides
926 def system_activities_and_project_overrides(include_inactive=false)
926 def system_activities_and_project_overrides(include_inactive=false)
927 if include_inactive
927 if include_inactive
928 return TimeEntryActivity.shared.
928 return TimeEntryActivity.shared.
929 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
929 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
930 self.time_entry_activities
930 self.time_entry_activities
931 else
931 else
932 return TimeEntryActivity.shared.active.
932 return TimeEntryActivity.shared.active.
933 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
933 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
934 self.time_entry_activities.active
934 self.time_entry_activities.active
935 end
935 end
936 end
936 end
937
937
938 # Archives subprojects recursively
938 # Archives subprojects recursively
939 def archive!
939 def archive!
940 children.each do |subproject|
940 children.each do |subproject|
941 subproject.send :archive!
941 subproject.send :archive!
942 end
942 end
943 update_attribute :status, STATUS_ARCHIVED
943 update_attribute :status, STATUS_ARCHIVED
944 end
944 end
945
945
946 def update_position_under_parent
946 def update_position_under_parent
947 set_or_update_position_under(parent)
947 set_or_update_position_under(parent)
948 end
948 end
949
949
950 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
950 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
951 def set_or_update_position_under(target_parent)
951 def set_or_update_position_under(target_parent)
952 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
952 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
953 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
953 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
954
954
955 if to_be_inserted_before
955 if to_be_inserted_before
956 move_to_left_of(to_be_inserted_before)
956 move_to_left_of(to_be_inserted_before)
957 elsif target_parent.nil?
957 elsif target_parent.nil?
958 if sibs.empty?
958 if sibs.empty?
959 # move_to_root adds the project in first (ie. left) position
959 # move_to_root adds the project in first (ie. left) position
960 move_to_root
960 move_to_root
961 else
961 else
962 move_to_right_of(sibs.last) unless self == sibs.last
962 move_to_right_of(sibs.last) unless self == sibs.last
963 end
963 end
964 else
964 else
965 # move_to_child_of adds the project in last (ie.right) position
965 # move_to_child_of adds the project in last (ie.right) position
966 move_to_child_of(target_parent)
966 move_to_child_of(target_parent)
967 end
967 end
968 end
968 end
969 end
969 end
@@ -1,438 +1,438
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 # Maximum length for repository identifiers
24 # Maximum length for repository identifiers
25 IDENTIFIER_MAX_LENGTH = 255
25 IDENTIFIER_MAX_LENGTH = 255
26
26
27 belongs_to :project
27 belongs_to :project
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30
30
31 serialize :extra_info
31 serialize :extra_info
32
32
33 before_save :check_default
33 before_save :check_default
34
34
35 # Raw SQL to delete changesets and changes in the database
35 # Raw SQL to delete changesets and changes in the database
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 before_destroy :clear_changesets
37 before_destroy :clear_changesets
38
38
39 validates_length_of :password, :maximum => 255, :allow_nil => true
39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
44 # donwcase letters, digits, dashes, underscores but not digits only
44 # donwcase letters, digits, dashes, underscores but not digits only
45 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 # Checks if the SCM is enabled when creating a repository
46 # Checks if the SCM is enabled when creating a repository
47 validate :repo_create_validation, :on => :create
47 validate :repo_create_validation, :on => :create
48
48
49 safe_attributes 'identifier',
49 safe_attributes 'identifier',
50 'login',
50 'login',
51 'password',
51 'password',
52 'path_encoding',
52 'path_encoding',
53 'log_encoding',
53 'log_encoding',
54 'is_default'
54 'is_default'
55
55
56 safe_attributes 'url',
56 safe_attributes 'url',
57 :if => lambda {|repository, user| repository.new_record?}
57 :if => lambda {|repository, user| repository.new_record?}
58
58
59 def repo_create_validation
59 def repo_create_validation
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 errors.add(:type, :invalid)
61 errors.add(:type, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def self.human_attribute_name(attribute_key_name, *args)
65 def self.human_attribute_name(attribute_key_name, *args)
66 attr_name = attribute_key_name.to_s
66 attr_name = attribute_key_name.to_s
67 if attr_name == "log_encoding"
67 if attr_name == "log_encoding"
68 attr_name = "commit_logs_encoding"
68 attr_name = "commit_logs_encoding"
69 end
69 end
70 super(attr_name, *args)
70 super(attr_name, *args)
71 end
71 end
72
72
73 # Removes leading and trailing whitespace
73 # Removes leading and trailing whitespace
74 def url=(arg)
74 def url=(arg)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 end
76 end
77
77
78 # Removes leading and trailing whitespace
78 # Removes leading and trailing whitespace
79 def root_url=(arg)
79 def root_url=(arg)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 end
81 end
82
82
83 def password
83 def password
84 read_ciphered_attribute(:password)
84 read_ciphered_attribute(:password)
85 end
85 end
86
86
87 def password=(arg)
87 def password=(arg)
88 write_ciphered_attribute(:password, arg)
88 write_ciphered_attribute(:password, arg)
89 end
89 end
90
90
91 def scm_adapter
91 def scm_adapter
92 self.class.scm_adapter_class
92 self.class.scm_adapter_class
93 end
93 end
94
94
95 def scm
95 def scm
96 unless @scm
96 unless @scm
97 @scm = self.scm_adapter.new(url, root_url,
97 @scm = self.scm_adapter.new(url, root_url,
98 login, password, path_encoding)
98 login, password, path_encoding)
99 if root_url.blank? && @scm.root_url.present?
99 if root_url.blank? && @scm.root_url.present?
100 update_attribute(:root_url, @scm.root_url)
100 update_attribute(:root_url, @scm.root_url)
101 end
101 end
102 end
102 end
103 @scm
103 @scm
104 end
104 end
105
105
106 def scm_name
106 def scm_name
107 self.class.scm_name
107 self.class.scm_name
108 end
108 end
109
109
110 def name
110 def name
111 if identifier.present?
111 if identifier.present?
112 identifier
112 identifier
113 elsif is_default?
113 elsif is_default?
114 l(:field_repository_is_default)
114 l(:field_repository_is_default)
115 else
115 else
116 scm_name
116 scm_name
117 end
117 end
118 end
118 end
119
119
120 def identifier=(identifier)
120 def identifier=(identifier)
121 super unless identifier_frozen?
121 super unless identifier_frozen?
122 end
122 end
123
123
124 def identifier_frozen?
124 def identifier_frozen?
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier.to_s <=> repository.identifier.to_s
144 identifier.to_s <=> repository.identifier.to_s
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 def merge_extra_info(arg)
156 def merge_extra_info(arg)
157 h = extra_info || {}
157 h = extra_info || {}
158 return h if arg.nil?
158 return h if arg.nil?
159 h.merge!(arg)
159 h.merge!(arg)
160 write_attribute(:extra_info, h)
160 write_attribute(:extra_info, h)
161 end
161 end
162
162
163 def report_last_commit
163 def report_last_commit
164 true
164 true
165 end
165 end
166
166
167 def supports_cat?
167 def supports_cat?
168 scm.supports_cat?
168 scm.supports_cat?
169 end
169 end
170
170
171 def supports_annotate?
171 def supports_annotate?
172 scm.supports_annotate?
172 scm.supports_annotate?
173 end
173 end
174
174
175 def supports_all_revisions?
175 def supports_all_revisions?
176 true
176 true
177 end
177 end
178
178
179 def supports_directory_revisions?
179 def supports_directory_revisions?
180 false
180 false
181 end
181 end
182
182
183 def supports_revision_graph?
183 def supports_revision_graph?
184 false
184 false
185 end
185 end
186
186
187 def entry(path=nil, identifier=nil)
187 def entry(path=nil, identifier=nil)
188 scm.entry(path, identifier)
188 scm.entry(path, identifier)
189 end
189 end
190
190
191 def entries(path=nil, identifier=nil)
191 def entries(path=nil, identifier=nil)
192 entries = scm.entries(path, identifier)
192 entries = scm.entries(path, identifier)
193 load_entries_changesets(entries)
193 load_entries_changesets(entries)
194 entries
194 entries
195 end
195 end
196
196
197 def branches
197 def branches
198 scm.branches
198 scm.branches
199 end
199 end
200
200
201 def tags
201 def tags
202 scm.tags
202 scm.tags
203 end
203 end
204
204
205 def default_branch
205 def default_branch
206 nil
206 nil
207 end
207 end
208
208
209 def properties(path, identifier=nil)
209 def properties(path, identifier=nil)
210 scm.properties(path, identifier)
210 scm.properties(path, identifier)
211 end
211 end
212
212
213 def cat(path, identifier=nil)
213 def cat(path, identifier=nil)
214 scm.cat(path, identifier)
214 scm.cat(path, identifier)
215 end
215 end
216
216
217 def diff(path, rev, rev_to)
217 def diff(path, rev, rev_to)
218 scm.diff(path, rev, rev_to)
218 scm.diff(path, rev, rev_to)
219 end
219 end
220
220
221 def diff_format_revisions(cs, cs_to, sep=':')
221 def diff_format_revisions(cs, cs_to, sep=':')
222 text = ""
222 text = ""
223 text << cs_to.format_identifier + sep if cs_to
223 text << cs_to.format_identifier + sep if cs_to
224 text << cs.format_identifier if cs
224 text << cs.format_identifier if cs
225 text
225 text
226 end
226 end
227
227
228 # Returns a path relative to the url of the repository
228 # Returns a path relative to the url of the repository
229 def relative_path(path)
229 def relative_path(path)
230 path
230 path
231 end
231 end
232
232
233 # Finds and returns a revision with a number or the beginning of a hash
233 # Finds and returns a revision with a number or the beginning of a hash
234 def find_changeset_by_name(name)
234 def find_changeset_by_name(name)
235 return nil if name.blank?
235 return nil if name.blank?
236 s = name.to_s
236 s = name.to_s
237 if s.match(/^\d*$/)
237 if s.match(/^\d*$/)
238 changesets.where("revision = ?", s).first
238 changesets.where("revision = ?", s).first
239 else
239 else
240 changesets.where("revision LIKE ?", s + '%').first
240 changesets.where("revision LIKE ?", s + '%').first
241 end
241 end
242 end
242 end
243
243
244 def latest_changeset
244 def latest_changeset
245 @latest_changeset ||= changesets.first
245 @latest_changeset ||= changesets.first
246 end
246 end
247
247
248 # Returns the latest changesets for +path+
248 # Returns the latest changesets for +path+
249 # Default behaviour is to search in cached changesets
249 # Default behaviour is to search in cached changesets
250 def latest_changesets(path, rev, limit=10)
250 def latest_changesets(path, rev, limit=10)
251 if path.blank?
251 if path.blank?
252 changesets.find(
252 changesets.find(
253 :all,
253 :all,
254 :include => :user,
254 :include => :user,
255 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
255 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
256 :limit => limit)
256 :limit => limit)
257 else
257 else
258 filechanges.find(
258 filechanges.find(
259 :all,
259 :all,
260 :include => {:changeset => :user},
260 :include => {:changeset => :user},
261 :conditions => ["path = ?", path.with_leading_slash],
261 :conditions => ["path = ?", path.with_leading_slash],
262 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
262 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
263 :limit => limit
263 :limit => limit
264 ).collect(&:changeset)
264 ).collect(&:changeset)
265 end
265 end
266 end
266 end
267
267
268 def scan_changesets_for_issue_ids
268 def scan_changesets_for_issue_ids
269 self.changesets.each(&:scan_comment_for_issue_ids)
269 self.changesets.each(&:scan_comment_for_issue_ids)
270 end
270 end
271
271
272 # Returns an array of committers usernames and associated user_id
272 # Returns an array of committers usernames and associated user_id
273 def committers
273 def committers
274 @committers ||= Changeset.connection.select_rows(
274 @committers ||= Changeset.connection.select_rows(
275 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
275 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
276 end
276 end
277
277
278 # Maps committers username to a user ids
278 # Maps committers username to a user ids
279 def committer_ids=(h)
279 def committer_ids=(h)
280 if h.is_a?(Hash)
280 if h.is_a?(Hash)
281 committers.each do |committer, user_id|
281 committers.each do |committer, user_id|
282 new_user_id = h[committer]
282 new_user_id = h[committer]
283 if new_user_id && (new_user_id.to_i != user_id.to_i)
283 if new_user_id && (new_user_id.to_i != user_id.to_i)
284 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
284 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
285 Changeset.update_all(
285 Changeset.update_all(
286 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
286 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
287 ["repository_id = ? AND committer = ?", id, committer])
287 ["repository_id = ? AND committer = ?", id, committer])
288 end
288 end
289 end
289 end
290 @committers = nil
290 @committers = nil
291 @found_committer_users = nil
291 @found_committer_users = nil
292 true
292 true
293 else
293 else
294 false
294 false
295 end
295 end
296 end
296 end
297
297
298 # Returns the Redmine User corresponding to the given +committer+
298 # Returns the Redmine User corresponding to the given +committer+
299 # It will return nil if the committer is not yet mapped and if no User
299 # It will return nil if the committer is not yet mapped and if no User
300 # with the same username or email was found
300 # with the same username or email was found
301 def find_committer_user(committer)
301 def find_committer_user(committer)
302 unless committer.blank?
302 unless committer.blank?
303 @found_committer_users ||= {}
303 @found_committer_users ||= {}
304 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
304 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
305
305
306 user = nil
306 user = nil
307 c = changesets.where(:committer => committer).includes(:user).first
307 c = changesets.where(:committer => committer).includes(:user).first
308 if c && c.user
308 if c && c.user
309 user = c.user
309 user = c.user
310 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
310 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
311 username, email = $1.strip, $3
311 username, email = $1.strip, $3
312 u = User.find_by_login(username)
312 u = User.find_by_login(username)
313 u ||= User.find_by_mail(email) unless email.blank?
313 u ||= User.find_by_mail(email) unless email.blank?
314 user = u
314 user = u
315 end
315 end
316 @found_committer_users[committer] = user
316 @found_committer_users[committer] = user
317 user
317 user
318 end
318 end
319 end
319 end
320
320
321 def repo_log_encoding
321 def repo_log_encoding
322 encoding = log_encoding.to_s.strip
322 encoding = log_encoding.to_s.strip
323 encoding.blank? ? 'UTF-8' : encoding
323 encoding.blank? ? 'UTF-8' : encoding
324 end
324 end
325
325
326 # Fetches new changesets for all repositories of active projects
326 # Fetches new changesets for all repositories of active projects
327 # Can be called periodically by an external script
327 # Can be called periodically by an external script
328 # eg. ruby script/runner "Repository.fetch_changesets"
328 # eg. ruby script/runner "Repository.fetch_changesets"
329 def self.fetch_changesets
329 def self.fetch_changesets
330 Project.active.has_module(:repository).all.each do |project|
330 Project.active.has_module(:repository).all.each do |project|
331 project.repositories.each do |repository|
331 project.repositories.each do |repository|
332 begin
332 begin
333 repository.fetch_changesets
333 repository.fetch_changesets
334 rescue Redmine::Scm::Adapters::CommandFailed => e
334 rescue Redmine::Scm::Adapters::CommandFailed => e
335 logger.error "scm: error during fetching changesets: #{e.message}"
335 logger.error "scm: error during fetching changesets: #{e.message}"
336 end
336 end
337 end
337 end
338 end
338 end
339 end
339 end
340
340
341 # scan changeset comments to find related and fixed issues for all repositories
341 # scan changeset comments to find related and fixed issues for all repositories
342 def self.scan_changesets_for_issue_ids
342 def self.scan_changesets_for_issue_ids
343 all.each(&:scan_changesets_for_issue_ids)
343 all.each(&:scan_changesets_for_issue_ids)
344 end
344 end
345
345
346 def self.scm_name
346 def self.scm_name
347 'Abstract'
347 'Abstract'
348 end
348 end
349
349
350 def self.available_scm
350 def self.available_scm
351 subclasses.collect {|klass| [klass.scm_name, klass.name]}
351 subclasses.collect {|klass| [klass.scm_name, klass.name]}
352 end
352 end
353
353
354 def self.factory(klass_name, *args)
354 def self.factory(klass_name, *args)
355 klass = "Repository::#{klass_name}".constantize
355 klass = "Repository::#{klass_name}".constantize
356 klass.new(*args)
356 klass.new(*args)
357 rescue
357 rescue
358 nil
358 nil
359 end
359 end
360
360
361 def self.scm_adapter_class
361 def self.scm_adapter_class
362 nil
362 nil
363 end
363 end
364
364
365 def self.scm_command
365 def self.scm_command
366 ret = ""
366 ret = ""
367 begin
367 begin
368 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
368 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
369 rescue Exception => e
369 rescue Exception => e
370 logger.error "scm: error during get command: #{e.message}"
370 logger.error "scm: error during get command: #{e.message}"
371 end
371 end
372 ret
372 ret
373 end
373 end
374
374
375 def self.scm_version_string
375 def self.scm_version_string
376 ret = ""
376 ret = ""
377 begin
377 begin
378 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
378 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
379 rescue Exception => e
379 rescue Exception => e
380 logger.error "scm: error during get version string: #{e.message}"
380 logger.error "scm: error during get version string: #{e.message}"
381 end
381 end
382 ret
382 ret
383 end
383 end
384
384
385 def self.scm_available
385 def self.scm_available
386 ret = false
386 ret = false
387 begin
387 begin
388 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
388 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
389 rescue Exception => e
389 rescue Exception => e
390 logger.error "scm: error during get scm available: #{e.message}"
390 logger.error "scm: error during get scm available: #{e.message}"
391 end
391 end
392 ret
392 ret
393 end
393 end
394
394
395 def set_as_default?
395 def set_as_default?
396 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
396 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
397 end
397 end
398
398
399 protected
399 protected
400
400
401 def check_default
401 def check_default
402 if !is_default? && set_as_default?
402 if !is_default? && set_as_default?
403 self.is_default = true
403 self.is_default = true
404 end
404 end
405 if is_default? && is_default_changed?
405 if is_default? && is_default_changed?
406 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
406 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
407 end
407 end
408 end
408 end
409
409
410 def load_entries_changesets(entries)
410 def load_entries_changesets(entries)
411 if entries
411 if entries
412 entries.each do |entry|
412 entries.each do |entry|
413 if entry.lastrev && entry.lastrev.identifier
413 if entry.lastrev && entry.lastrev.identifier
414 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
414 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
415 end
415 end
416 end
416 end
417 end
417 end
418 end
418 end
419
419
420 private
420 private
421
421
422 # Deletes repository data
422 # Deletes repository data
423 def clear_changesets
423 def clear_changesets
424 cs = Changeset.table_name
424 cs = Changeset.table_name
425 ch = Change.table_name
425 ch = Change.table_name
426 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
426 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
427 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
427 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
428
428
429 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
429 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
430 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
430 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
431 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
431 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
432 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
432 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
433 clear_extra_info_of_changesets
433 clear_extra_info_of_changesets
434 end
434 end
435
435
436 def clear_extra_info_of_changesets
436 def clear_extra_info_of_changesets
437 end
437 end
438 end
438 end
@@ -1,116 +1,116
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/subversion_adapter'
18 require 'redmine/scm/adapters/subversion_adapter'
19
19
20 class Repository::Subversion < Repository
20 class Repository::Subversion < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23 validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
23 validates_format_of :url, :with => /\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
24
24
25 def self.scm_adapter_class
25 def self.scm_adapter_class
26 Redmine::Scm::Adapters::SubversionAdapter
26 Redmine::Scm::Adapters::SubversionAdapter
27 end
27 end
28
28
29 def self.scm_name
29 def self.scm_name
30 'Subversion'
30 'Subversion'
31 end
31 end
32
32
33 def supports_directory_revisions?
33 def supports_directory_revisions?
34 true
34 true
35 end
35 end
36
36
37 def repo_log_encoding
37 def repo_log_encoding
38 'UTF-8'
38 'UTF-8'
39 end
39 end
40
40
41 def latest_changesets(path, rev, limit=10)
41 def latest_changesets(path, rev, limit=10)
42 revisions = scm.revisions(path, rev, nil, :limit => limit)
42 revisions = scm.revisions(path, rev, nil, :limit => limit)
43 if revisions
43 if revisions
44 identifiers = revisions.collect(&:identifier).compact
44 identifiers = revisions.collect(&:identifier).compact
45 changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all
45 changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all
46 else
46 else
47 []
47 []
48 end
48 end
49 end
49 end
50
50
51 # Returns a path relative to the url of the repository
51 # Returns a path relative to the url of the repository
52 def relative_path(path)
52 def relative_path(path)
53 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
53 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
54 end
54 end
55
55
56 def fetch_changesets
56 def fetch_changesets
57 scm_info = scm.info
57 scm_info = scm.info
58 if scm_info
58 if scm_info
59 # latest revision found in database
59 # latest revision found in database
60 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
60 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
61 # latest revision in the repository
61 # latest revision in the repository
62 scm_revision = scm_info.lastrev.identifier.to_i
62 scm_revision = scm_info.lastrev.identifier.to_i
63 if db_revision < scm_revision
63 if db_revision < scm_revision
64 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
64 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
65 identifier_from = db_revision + 1
65 identifier_from = db_revision + 1
66 while (identifier_from <= scm_revision)
66 while (identifier_from <= scm_revision)
67 # loads changesets by batches of 200
67 # loads changesets by batches of 200
68 identifier_to = [identifier_from + 199, scm_revision].min
68 identifier_to = [identifier_from + 199, scm_revision].min
69 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
69 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
70 revisions.reverse_each do |revision|
70 revisions.reverse_each do |revision|
71 transaction do
71 transaction do
72 changeset = Changeset.create(:repository => self,
72 changeset = Changeset.create(:repository => self,
73 :revision => revision.identifier,
73 :revision => revision.identifier,
74 :committer => revision.author,
74 :committer => revision.author,
75 :committed_on => revision.time,
75 :committed_on => revision.time,
76 :comments => revision.message)
76 :comments => revision.message)
77
77
78 revision.paths.each do |change|
78 revision.paths.each do |change|
79 changeset.create_change(change)
79 changeset.create_change(change)
80 end unless changeset.new_record?
80 end unless changeset.new_record?
81 end
81 end
82 end unless revisions.nil?
82 end unless revisions.nil?
83 identifier_from = identifier_to + 1
83 identifier_from = identifier_to + 1
84 end
84 end
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 protected
89 protected
90
90
91 def load_entries_changesets(entries)
91 def load_entries_changesets(entries)
92 return unless entries
92 return unless entries
93
93
94 entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?}
94 entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?}
95 identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq
95 identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq
96
96
97 if identifiers.any?
97 if identifiers.any?
98 changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision)
98 changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision)
99 entries_with_identifier.each do |entry|
99 entries_with_identifier.each do |entry|
100 if m = changesets_by_identifier[entry.lastrev.identifier]
100 if m = changesets_by_identifier[entry.lastrev.identifier]
101 entry.changeset = m.first
101 entry.changeset = m.first
102 end
102 end
103 end
103 end
104 end
104 end
105 end
105 end
106
106
107 private
107 private
108
108
109 # Returns the relative url of the repository
109 # Returns the relative url of the repository
110 # Eg: root_url = file:///var/svn/foo
110 # Eg: root_url = file:///var/svn/foo
111 # url = file:///var/svn/foo/bar
111 # url = file:///var/svn/foo/bar
112 # => returns /bar
112 # => returns /bar
113 def relative_url
113 def relative_url
114 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
114 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
115 end
115 end
116 end
116 end
@@ -1,706 +1,706
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation
84 attr_accessor :password, :password_confirmation
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain lettres, numbers, underscores only
95 # Login must contain lettres, numbers, underscores only
96 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :update_hashed_password
106 before_save :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108
108
109 scope :in_group, lambda {|group|
109 scope :in_group, lambda {|group|
110 group_id = group.is_a?(Group) ? group.id : group.to_i
110 group_id = group.is_a?(Group) ? group.id : group.to_i
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 }
112 }
113 scope :not_in_group, lambda {|group|
113 scope :not_in_group, lambda {|group|
114 group_id = group.is_a?(Group) ? group.id : group.to_i
114 group_id = group.is_a?(Group) ? group.id : group.to_i
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 }
116 }
117
117
118 def set_mail_notification
118 def set_mail_notification
119 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
119 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 true
120 true
121 end
121 end
122
122
123 def update_hashed_password
123 def update_hashed_password
124 # update hashed_password if password was set
124 # update hashed_password if password was set
125 if self.password && self.auth_source_id.blank?
125 if self.password && self.auth_source_id.blank?
126 salt_password(password)
126 salt_password(password)
127 end
127 end
128 end
128 end
129
129
130 def reload(*args)
130 def reload(*args)
131 @name = nil
131 @name = nil
132 @projects_by_role = nil
132 @projects_by_role = nil
133 super
133 super
134 end
134 end
135
135
136 def mail=(arg)
136 def mail=(arg)
137 write_attribute(:mail, arg.to_s.strip)
137 write_attribute(:mail, arg.to_s.strip)
138 end
138 end
139
139
140 def identity_url=(url)
140 def identity_url=(url)
141 if url.blank?
141 if url.blank?
142 write_attribute(:identity_url, '')
142 write_attribute(:identity_url, '')
143 else
143 else
144 begin
144 begin
145 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
145 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
146 rescue OpenIdAuthentication::InvalidOpenId
146 rescue OpenIdAuthentication::InvalidOpenId
147 # Invlaid url, don't save
147 # Invlaid url, don't save
148 end
148 end
149 end
149 end
150 self.read_attribute(:identity_url)
150 self.read_attribute(:identity_url)
151 end
151 end
152
152
153 # Returns the user that matches provided login and password, or nil
153 # Returns the user that matches provided login and password, or nil
154 def self.try_to_login(login, password)
154 def self.try_to_login(login, password)
155 login = login.to_s
155 login = login.to_s
156 password = password.to_s
156 password = password.to_s
157
157
158 # Make sure no one can sign in with an empty password
158 # Make sure no one can sign in with an empty password
159 return nil if password.empty?
159 return nil if password.empty?
160 user = find_by_login(login)
160 user = find_by_login(login)
161 if user
161 if user
162 # user is already in local database
162 # user is already in local database
163 return nil if !user.active?
163 return nil if !user.active?
164 if user.auth_source
164 if user.auth_source
165 # user has an external authentication method
165 # user has an external authentication method
166 return nil unless user.auth_source.authenticate(login, password)
166 return nil unless user.auth_source.authenticate(login, password)
167 else
167 else
168 # authentication with local password
168 # authentication with local password
169 return nil unless user.check_password?(password)
169 return nil unless user.check_password?(password)
170 end
170 end
171 else
171 else
172 # user is not yet registered, try to authenticate with available sources
172 # user is not yet registered, try to authenticate with available sources
173 attrs = AuthSource.authenticate(login, password)
173 attrs = AuthSource.authenticate(login, password)
174 if attrs
174 if attrs
175 user = new(attrs)
175 user = new(attrs)
176 user.login = login
176 user.login = login
177 user.language = Setting.default_language
177 user.language = Setting.default_language
178 if user.save
178 if user.save
179 user.reload
179 user.reload
180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 end
181 end
182 end
182 end
183 end
183 end
184 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
184 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
185 user
185 user
186 rescue => text
186 rescue => text
187 raise text
187 raise text
188 end
188 end
189
189
190 # Returns the user who matches the given autologin +key+ or nil
190 # Returns the user who matches the given autologin +key+ or nil
191 def self.try_to_autologin(key)
191 def self.try_to_autologin(key)
192 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
192 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
193 # Make sure there's only 1 token that matches the key
193 # Make sure there's only 1 token that matches the key
194 if tokens.size == 1
194 if tokens.size == 1
195 token = tokens.first
195 token = tokens.first
196 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
196 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
197 token.user.update_attribute(:last_login_on, Time.now)
197 token.user.update_attribute(:last_login_on, Time.now)
198 token.user
198 token.user
199 end
199 end
200 end
200 end
201 end
201 end
202
202
203 def self.name_formatter(formatter = nil)
203 def self.name_formatter(formatter = nil)
204 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
204 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
205 end
205 end
206
206
207 # Returns an array of fields names than can be used to make an order statement for users
207 # Returns an array of fields names than can be used to make an order statement for users
208 # according to how user names are displayed
208 # according to how user names are displayed
209 # Examples:
209 # Examples:
210 #
210 #
211 # User.fields_for_order_statement => ['users.login', 'users.id']
211 # User.fields_for_order_statement => ['users.login', 'users.id']
212 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
212 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
213 def self.fields_for_order_statement(table=nil)
213 def self.fields_for_order_statement(table=nil)
214 table ||= table_name
214 table ||= table_name
215 name_formatter[:order].map {|field| "#{table}.#{field}"}
215 name_formatter[:order].map {|field| "#{table}.#{field}"}
216 end
216 end
217
217
218 # Return user's full name for display
218 # Return user's full name for display
219 def name(formatter = nil)
219 def name(formatter = nil)
220 f = self.class.name_formatter(formatter)
220 f = self.class.name_formatter(formatter)
221 if formatter
221 if formatter
222 eval('"' + f[:string] + '"')
222 eval('"' + f[:string] + '"')
223 else
223 else
224 @name ||= eval('"' + f[:string] + '"')
224 @name ||= eval('"' + f[:string] + '"')
225 end
225 end
226 end
226 end
227
227
228 def active?
228 def active?
229 self.status == STATUS_ACTIVE
229 self.status == STATUS_ACTIVE
230 end
230 end
231
231
232 def registered?
232 def registered?
233 self.status == STATUS_REGISTERED
233 self.status == STATUS_REGISTERED
234 end
234 end
235
235
236 def locked?
236 def locked?
237 self.status == STATUS_LOCKED
237 self.status == STATUS_LOCKED
238 end
238 end
239
239
240 def activate
240 def activate
241 self.status = STATUS_ACTIVE
241 self.status = STATUS_ACTIVE
242 end
242 end
243
243
244 def register
244 def register
245 self.status = STATUS_REGISTERED
245 self.status = STATUS_REGISTERED
246 end
246 end
247
247
248 def lock
248 def lock
249 self.status = STATUS_LOCKED
249 self.status = STATUS_LOCKED
250 end
250 end
251
251
252 def activate!
252 def activate!
253 update_attribute(:status, STATUS_ACTIVE)
253 update_attribute(:status, STATUS_ACTIVE)
254 end
254 end
255
255
256 def register!
256 def register!
257 update_attribute(:status, STATUS_REGISTERED)
257 update_attribute(:status, STATUS_REGISTERED)
258 end
258 end
259
259
260 def lock!
260 def lock!
261 update_attribute(:status, STATUS_LOCKED)
261 update_attribute(:status, STATUS_LOCKED)
262 end
262 end
263
263
264 # Returns true if +clear_password+ is the correct user's password, otherwise false
264 # Returns true if +clear_password+ is the correct user's password, otherwise false
265 def check_password?(clear_password)
265 def check_password?(clear_password)
266 if auth_source_id.present?
266 if auth_source_id.present?
267 auth_source.authenticate(self.login, clear_password)
267 auth_source.authenticate(self.login, clear_password)
268 else
268 else
269 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
269 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
270 end
270 end
271 end
271 end
272
272
273 # Generates a random salt and computes hashed_password for +clear_password+
273 # Generates a random salt and computes hashed_password for +clear_password+
274 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
274 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
275 def salt_password(clear_password)
275 def salt_password(clear_password)
276 self.salt = User.generate_salt
276 self.salt = User.generate_salt
277 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
277 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
278 end
278 end
279
279
280 # Does the backend storage allow this user to change their password?
280 # Does the backend storage allow this user to change their password?
281 def change_password_allowed?
281 def change_password_allowed?
282 return true if auth_source.nil?
282 return true if auth_source.nil?
283 return auth_source.allow_password_changes?
283 return auth_source.allow_password_changes?
284 end
284 end
285
285
286 # Generate and set a random password. Useful for automated user creation
286 # Generate and set a random password. Useful for automated user creation
287 # Based on Token#generate_token_value
287 # Based on Token#generate_token_value
288 #
288 #
289 def random_password
289 def random_password
290 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
290 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
291 password = ''
291 password = ''
292 40.times { |i| password << chars[rand(chars.size-1)] }
292 40.times { |i| password << chars[rand(chars.size-1)] }
293 self.password = password
293 self.password = password
294 self.password_confirmation = password
294 self.password_confirmation = password
295 self
295 self
296 end
296 end
297
297
298 def pref
298 def pref
299 self.preference ||= UserPreference.new(:user => self)
299 self.preference ||= UserPreference.new(:user => self)
300 end
300 end
301
301
302 def time_zone
302 def time_zone
303 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
303 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
304 end
304 end
305
305
306 def wants_comments_in_reverse_order?
306 def wants_comments_in_reverse_order?
307 self.pref[:comments_sorting] == 'desc'
307 self.pref[:comments_sorting] == 'desc'
308 end
308 end
309
309
310 # Return user's RSS key (a 40 chars long string), used to access feeds
310 # Return user's RSS key (a 40 chars long string), used to access feeds
311 def rss_key
311 def rss_key
312 if rss_token.nil?
312 if rss_token.nil?
313 create_rss_token(:action => 'feeds')
313 create_rss_token(:action => 'feeds')
314 end
314 end
315 rss_token.value
315 rss_token.value
316 end
316 end
317
317
318 # Return user's API key (a 40 chars long string), used to access the API
318 # Return user's API key (a 40 chars long string), used to access the API
319 def api_key
319 def api_key
320 if api_token.nil?
320 if api_token.nil?
321 create_api_token(:action => 'api')
321 create_api_token(:action => 'api')
322 end
322 end
323 api_token.value
323 api_token.value
324 end
324 end
325
325
326 # Return an array of project ids for which the user has explicitly turned mail notifications on
326 # Return an array of project ids for which the user has explicitly turned mail notifications on
327 def notified_projects_ids
327 def notified_projects_ids
328 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
328 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
329 end
329 end
330
330
331 def notified_project_ids=(ids)
331 def notified_project_ids=(ids)
332 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
332 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
333 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
333 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
334 @notified_projects_ids = nil
334 @notified_projects_ids = nil
335 notified_projects_ids
335 notified_projects_ids
336 end
336 end
337
337
338 def valid_notification_options
338 def valid_notification_options
339 self.class.valid_notification_options(self)
339 self.class.valid_notification_options(self)
340 end
340 end
341
341
342 # Only users that belong to more than 1 project can select projects for which they are notified
342 # Only users that belong to more than 1 project can select projects for which they are notified
343 def self.valid_notification_options(user=nil)
343 def self.valid_notification_options(user=nil)
344 # Note that @user.membership.size would fail since AR ignores
344 # Note that @user.membership.size would fail since AR ignores
345 # :include association option when doing a count
345 # :include association option when doing a count
346 if user.nil? || user.memberships.length < 1
346 if user.nil? || user.memberships.length < 1
347 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
347 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
348 else
348 else
349 MAIL_NOTIFICATION_OPTIONS
349 MAIL_NOTIFICATION_OPTIONS
350 end
350 end
351 end
351 end
352
352
353 # Find a user account by matching the exact login and then a case-insensitive
353 # Find a user account by matching the exact login and then a case-insensitive
354 # version. Exact matches will be given priority.
354 # version. Exact matches will be given priority.
355 def self.find_by_login(login)
355 def self.find_by_login(login)
356 # First look for an exact match
356 # First look for an exact match
357 user = where(:login => login).all.detect {|u| u.login == login}
357 user = where(:login => login).all.detect {|u| u.login == login}
358 unless user
358 unless user
359 # Fail over to case-insensitive if none was found
359 # Fail over to case-insensitive if none was found
360 user = where("LOWER(login) = ?", login.to_s.downcase).first
360 user = where("LOWER(login) = ?", login.to_s.downcase).first
361 end
361 end
362 user
362 user
363 end
363 end
364
364
365 def self.find_by_rss_key(key)
365 def self.find_by_rss_key(key)
366 token = Token.find_by_action_and_value('feeds', key.to_s)
366 token = Token.find_by_action_and_value('feeds', key.to_s)
367 token && token.user.active? ? token.user : nil
367 token && token.user.active? ? token.user : nil
368 end
368 end
369
369
370 def self.find_by_api_key(key)
370 def self.find_by_api_key(key)
371 token = Token.find_by_action_and_value('api', key.to_s)
371 token = Token.find_by_action_and_value('api', key.to_s)
372 token && token.user.active? ? token.user : nil
372 token && token.user.active? ? token.user : nil
373 end
373 end
374
374
375 # Makes find_by_mail case-insensitive
375 # Makes find_by_mail case-insensitive
376 def self.find_by_mail(mail)
376 def self.find_by_mail(mail)
377 where("LOWER(mail) = ?", mail.to_s.downcase).first
377 where("LOWER(mail) = ?", mail.to_s.downcase).first
378 end
378 end
379
379
380 # Returns true if the default admin account can no longer be used
380 # Returns true if the default admin account can no longer be used
381 def self.default_admin_account_changed?
381 def self.default_admin_account_changed?
382 !User.active.find_by_login("admin").try(:check_password?, "admin")
382 !User.active.find_by_login("admin").try(:check_password?, "admin")
383 end
383 end
384
384
385 def to_s
385 def to_s
386 name
386 name
387 end
387 end
388
388
389 CSS_CLASS_BY_STATUS = {
389 CSS_CLASS_BY_STATUS = {
390 STATUS_ANONYMOUS => 'anon',
390 STATUS_ANONYMOUS => 'anon',
391 STATUS_ACTIVE => 'active',
391 STATUS_ACTIVE => 'active',
392 STATUS_REGISTERED => 'registered',
392 STATUS_REGISTERED => 'registered',
393 STATUS_LOCKED => 'locked'
393 STATUS_LOCKED => 'locked'
394 }
394 }
395
395
396 def css_classes
396 def css_classes
397 "user #{CSS_CLASS_BY_STATUS[status]}"
397 "user #{CSS_CLASS_BY_STATUS[status]}"
398 end
398 end
399
399
400 # Returns the current day according to user's time zone
400 # Returns the current day according to user's time zone
401 def today
401 def today
402 if time_zone.nil?
402 if time_zone.nil?
403 Date.today
403 Date.today
404 else
404 else
405 Time.now.in_time_zone(time_zone).to_date
405 Time.now.in_time_zone(time_zone).to_date
406 end
406 end
407 end
407 end
408
408
409 # Returns the day of +time+ according to user's time zone
409 # Returns the day of +time+ according to user's time zone
410 def time_to_date(time)
410 def time_to_date(time)
411 if time_zone.nil?
411 if time_zone.nil?
412 time.to_date
412 time.to_date
413 else
413 else
414 time.in_time_zone(time_zone).to_date
414 time.in_time_zone(time_zone).to_date
415 end
415 end
416 end
416 end
417
417
418 def logged?
418 def logged?
419 true
419 true
420 end
420 end
421
421
422 def anonymous?
422 def anonymous?
423 !logged?
423 !logged?
424 end
424 end
425
425
426 # Return user's roles for project
426 # Return user's roles for project
427 def roles_for_project(project)
427 def roles_for_project(project)
428 roles = []
428 roles = []
429 # No role on archived projects
429 # No role on archived projects
430 return roles if project.nil? || project.archived?
430 return roles if project.nil? || project.archived?
431 if logged?
431 if logged?
432 # Find project membership
432 # Find project membership
433 membership = memberships.detect {|m| m.project_id == project.id}
433 membership = memberships.detect {|m| m.project_id == project.id}
434 if membership
434 if membership
435 roles = membership.roles
435 roles = membership.roles
436 else
436 else
437 @role_non_member ||= Role.non_member
437 @role_non_member ||= Role.non_member
438 roles << @role_non_member
438 roles << @role_non_member
439 end
439 end
440 else
440 else
441 @role_anonymous ||= Role.anonymous
441 @role_anonymous ||= Role.anonymous
442 roles << @role_anonymous
442 roles << @role_anonymous
443 end
443 end
444 roles
444 roles
445 end
445 end
446
446
447 # Return true if the user is a member of project
447 # Return true if the user is a member of project
448 def member_of?(project)
448 def member_of?(project)
449 !roles_for_project(project).detect {|role| role.member?}.nil?
449 !roles_for_project(project).detect {|role| role.member?}.nil?
450 end
450 end
451
451
452 # Returns a hash of user's projects grouped by roles
452 # Returns a hash of user's projects grouped by roles
453 def projects_by_role
453 def projects_by_role
454 return @projects_by_role if @projects_by_role
454 return @projects_by_role if @projects_by_role
455
455
456 @projects_by_role = Hash.new([])
456 @projects_by_role = Hash.new([])
457 memberships.each do |membership|
457 memberships.each do |membership|
458 if membership.project
458 if membership.project
459 membership.roles.each do |role|
459 membership.roles.each do |role|
460 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
460 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
461 @projects_by_role[role] << membership.project
461 @projects_by_role[role] << membership.project
462 end
462 end
463 end
463 end
464 end
464 end
465 @projects_by_role.each do |role, projects|
465 @projects_by_role.each do |role, projects|
466 projects.uniq!
466 projects.uniq!
467 end
467 end
468
468
469 @projects_by_role
469 @projects_by_role
470 end
470 end
471
471
472 # Returns true if user is arg or belongs to arg
472 # Returns true if user is arg or belongs to arg
473 def is_or_belongs_to?(arg)
473 def is_or_belongs_to?(arg)
474 if arg.is_a?(User)
474 if arg.is_a?(User)
475 self == arg
475 self == arg
476 elsif arg.is_a?(Group)
476 elsif arg.is_a?(Group)
477 arg.users.include?(self)
477 arg.users.include?(self)
478 else
478 else
479 false
479 false
480 end
480 end
481 end
481 end
482
482
483 # Return true if the user is allowed to do the specified action on a specific context
483 # Return true if the user is allowed to do the specified action on a specific context
484 # Action can be:
484 # Action can be:
485 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
485 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
486 # * a permission Symbol (eg. :edit_project)
486 # * a permission Symbol (eg. :edit_project)
487 # Context can be:
487 # Context can be:
488 # * a project : returns true if user is allowed to do the specified action on this project
488 # * a project : returns true if user is allowed to do the specified action on this project
489 # * an array of projects : returns true if user is allowed on every project
489 # * an array of projects : returns true if user is allowed on every project
490 # * nil with options[:global] set : check if user has at least one role allowed for this action,
490 # * nil with options[:global] set : check if user has at least one role allowed for this action,
491 # or falls back to Non Member / Anonymous permissions depending if the user is logged
491 # or falls back to Non Member / Anonymous permissions depending if the user is logged
492 def allowed_to?(action, context, options={}, &block)
492 def allowed_to?(action, context, options={}, &block)
493 if context && context.is_a?(Project)
493 if context && context.is_a?(Project)
494 return false unless context.allows_to?(action)
494 return false unless context.allows_to?(action)
495 # Admin users are authorized for anything else
495 # Admin users are authorized for anything else
496 return true if admin?
496 return true if admin?
497
497
498 roles = roles_for_project(context)
498 roles = roles_for_project(context)
499 return false unless roles
499 return false unless roles
500 roles.any? {|role|
500 roles.any? {|role|
501 (context.is_public? || role.member?) &&
501 (context.is_public? || role.member?) &&
502 role.allowed_to?(action) &&
502 role.allowed_to?(action) &&
503 (block_given? ? yield(role, self) : true)
503 (block_given? ? yield(role, self) : true)
504 }
504 }
505 elsif context && context.is_a?(Array)
505 elsif context && context.is_a?(Array)
506 if context.empty?
506 if context.empty?
507 false
507 false
508 else
508 else
509 # Authorize if user is authorized on every element of the array
509 # Authorize if user is authorized on every element of the array
510 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
510 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
511 end
511 end
512 elsif options[:global]
512 elsif options[:global]
513 # Admin users are always authorized
513 # Admin users are always authorized
514 return true if admin?
514 return true if admin?
515
515
516 # authorize if user has at least one role that has this permission
516 # authorize if user has at least one role that has this permission
517 roles = memberships.collect {|m| m.roles}.flatten.uniq
517 roles = memberships.collect {|m| m.roles}.flatten.uniq
518 roles << (self.logged? ? Role.non_member : Role.anonymous)
518 roles << (self.logged? ? Role.non_member : Role.anonymous)
519 roles.any? {|role|
519 roles.any? {|role|
520 role.allowed_to?(action) &&
520 role.allowed_to?(action) &&
521 (block_given? ? yield(role, self) : true)
521 (block_given? ? yield(role, self) : true)
522 }
522 }
523 else
523 else
524 false
524 false
525 end
525 end
526 end
526 end
527
527
528 # Is the user allowed to do the specified action on any project?
528 # Is the user allowed to do the specified action on any project?
529 # See allowed_to? for the actions and valid options.
529 # See allowed_to? for the actions and valid options.
530 def allowed_to_globally?(action, options, &block)
530 def allowed_to_globally?(action, options, &block)
531 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
531 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
532 end
532 end
533
533
534 # Returns true if the user is allowed to delete his own account
534 # Returns true if the user is allowed to delete his own account
535 def own_account_deletable?
535 def own_account_deletable?
536 Setting.unsubscribe? &&
536 Setting.unsubscribe? &&
537 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
537 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
538 end
538 end
539
539
540 safe_attributes 'login',
540 safe_attributes 'login',
541 'firstname',
541 'firstname',
542 'lastname',
542 'lastname',
543 'mail',
543 'mail',
544 'mail_notification',
544 'mail_notification',
545 'language',
545 'language',
546 'custom_field_values',
546 'custom_field_values',
547 'custom_fields',
547 'custom_fields',
548 'identity_url'
548 'identity_url'
549
549
550 safe_attributes 'status',
550 safe_attributes 'status',
551 'auth_source_id',
551 'auth_source_id',
552 :if => lambda {|user, current_user| current_user.admin?}
552 :if => lambda {|user, current_user| current_user.admin?}
553
553
554 safe_attributes 'group_ids',
554 safe_attributes 'group_ids',
555 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
555 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
556
556
557 # Utility method to help check if a user should be notified about an
557 # Utility method to help check if a user should be notified about an
558 # event.
558 # event.
559 #
559 #
560 # TODO: only supports Issue events currently
560 # TODO: only supports Issue events currently
561 def notify_about?(object)
561 def notify_about?(object)
562 case mail_notification
562 case mail_notification
563 when 'all'
563 when 'all'
564 true
564 true
565 when 'selected'
565 when 'selected'
566 # user receives notifications for created/assigned issues on unselected projects
566 # user receives notifications for created/assigned issues on unselected projects
567 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
567 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
568 true
568 true
569 else
569 else
570 false
570 false
571 end
571 end
572 when 'none'
572 when 'none'
573 false
573 false
574 when 'only_my_events'
574 when 'only_my_events'
575 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
575 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
576 true
576 true
577 else
577 else
578 false
578 false
579 end
579 end
580 when 'only_assigned'
580 when 'only_assigned'
581 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
581 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
582 true
582 true
583 else
583 else
584 false
584 false
585 end
585 end
586 when 'only_owner'
586 when 'only_owner'
587 if object.is_a?(Issue) && object.author == self
587 if object.is_a?(Issue) && object.author == self
588 true
588 true
589 else
589 else
590 false
590 false
591 end
591 end
592 else
592 else
593 false
593 false
594 end
594 end
595 end
595 end
596
596
597 def self.current=(user)
597 def self.current=(user)
598 Thread.current[:current_user] = user
598 Thread.current[:current_user] = user
599 end
599 end
600
600
601 def self.current
601 def self.current
602 Thread.current[:current_user] ||= User.anonymous
602 Thread.current[:current_user] ||= User.anonymous
603 end
603 end
604
604
605 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
605 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
606 # one anonymous user per database.
606 # one anonymous user per database.
607 def self.anonymous
607 def self.anonymous
608 anonymous_user = AnonymousUser.first
608 anonymous_user = AnonymousUser.first
609 if anonymous_user.nil?
609 if anonymous_user.nil?
610 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
610 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
611 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
611 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
612 end
612 end
613 anonymous_user
613 anonymous_user
614 end
614 end
615
615
616 # Salts all existing unsalted passwords
616 # Salts all existing unsalted passwords
617 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
617 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
618 # This method is used in the SaltPasswords migration and is to be kept as is
618 # This method is used in the SaltPasswords migration and is to be kept as is
619 def self.salt_unsalted_passwords!
619 def self.salt_unsalted_passwords!
620 transaction do
620 transaction do
621 User.where("salt IS NULL OR salt = ''").find_each do |user|
621 User.where("salt IS NULL OR salt = ''").find_each do |user|
622 next if user.hashed_password.blank?
622 next if user.hashed_password.blank?
623 salt = User.generate_salt
623 salt = User.generate_salt
624 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
624 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
625 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
625 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
626 end
626 end
627 end
627 end
628 end
628 end
629
629
630 protected
630 protected
631
631
632 def validate_password_length
632 def validate_password_length
633 # Password length validation based on setting
633 # Password length validation based on setting
634 if !password.nil? && password.size < Setting.password_min_length.to_i
634 if !password.nil? && password.size < Setting.password_min_length.to_i
635 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
635 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
636 end
636 end
637 end
637 end
638
638
639 private
639 private
640
640
641 # Removes references that are not handled by associations
641 # Removes references that are not handled by associations
642 # Things that are not deleted are reassociated with the anonymous user
642 # Things that are not deleted are reassociated with the anonymous user
643 def remove_references_before_destroy
643 def remove_references_before_destroy
644 return if self.id.nil?
644 return if self.id.nil?
645
645
646 substitute = User.anonymous
646 substitute = User.anonymous
647 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
647 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
649 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
649 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
650 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
650 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
651 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
651 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
652 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
653 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
653 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
654 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
654 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 # Remove private queries and keep public ones
656 # Remove private queries and keep public ones
657 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
657 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
658 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
658 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
659 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
659 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
660 Token.delete_all ['user_id = ?', id]
660 Token.delete_all ['user_id = ?', id]
661 Watcher.delete_all ['user_id = ?', id]
661 Watcher.delete_all ['user_id = ?', id]
662 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
662 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
663 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
663 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
664 end
664 end
665
665
666 # Return password digest
666 # Return password digest
667 def self.hash_password(clear_password)
667 def self.hash_password(clear_password)
668 Digest::SHA1.hexdigest(clear_password || "")
668 Digest::SHA1.hexdigest(clear_password || "")
669 end
669 end
670
670
671 # Returns a 128bits random salt as a hex string (32 chars long)
671 # Returns a 128bits random salt as a hex string (32 chars long)
672 def self.generate_salt
672 def self.generate_salt
673 Redmine::Utils.random_hex(16)
673 Redmine::Utils.random_hex(16)
674 end
674 end
675
675
676 end
676 end
677
677
678 class AnonymousUser < User
678 class AnonymousUser < User
679 validate :validate_anonymous_uniqueness, :on => :create
679 validate :validate_anonymous_uniqueness, :on => :create
680
680
681 def validate_anonymous_uniqueness
681 def validate_anonymous_uniqueness
682 # There should be only one AnonymousUser in the database
682 # There should be only one AnonymousUser in the database
683 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
683 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
684 end
684 end
685
685
686 def available_custom_fields
686 def available_custom_fields
687 []
687 []
688 end
688 end
689
689
690 # Overrides a few properties
690 # Overrides a few properties
691 def logged?; false end
691 def logged?; false end
692 def admin; false end
692 def admin; false end
693 def name(*args); I18n.t(:label_user_anonymous) end
693 def name(*args); I18n.t(:label_user_anonymous) end
694 def mail; nil end
694 def mail; nil end
695 def time_zone; nil end
695 def time_zone; nil end
696 def rss_key; nil end
696 def rss_key; nil end
697
697
698 def pref
698 def pref
699 UserPreference.new(:user => self)
699 UserPreference.new(:user => self)
700 end
700 end
701
701
702 # Anonymous user can not be destroyed
702 # Anonymous user can not be destroyed
703 def destroy
703 def destroy
704 false
704 false
705 end
705 end
706 end
706 end
@@ -1,284 +1,284
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 after_update :update_issues_from_sharing_change
20 after_update :update_issues_from_sharing_change
21 belongs_to :project
21 belongs_to :project
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
23 acts_as_customizable
23 acts_as_customizable
24 acts_as_attachable :view_permission => :view_files,
24 acts_as_attachable :view_permission => :view_files,
25 :delete_permission => :manage_files
25 :delete_permission => :manage_files
26
26
27 VERSION_STATUSES = %w(open locked closed)
27 VERSION_STATUSES = %w(open locked closed)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29
29
30 validates_presence_of :name
30 validates_presence_of :name
31 validates_uniqueness_of :name, :scope => [:project_id]
31 validates_uniqueness_of :name, :scope => [:project_id]
32 validates_length_of :name, :maximum => 60
32 validates_length_of :name, :maximum => 60
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 validates_format_of :effective_date, :with => /\A\d{4}-\d{2}-\d{2}\z/, :message => :not_a_date, :allow_nil => true
34 validates_inclusion_of :status, :in => VERSION_STATUSES
34 validates_inclusion_of :status, :in => VERSION_STATUSES
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
36 validate :validate_version
36 validate :validate_version
37
37
38 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
38 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
39 scope :open, lambda { where(:status => 'open') }
39 scope :open, lambda { where(:status => 'open') }
40 scope :visible, lambda {|*args|
40 scope :visible, lambda {|*args|
41 includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
41 includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
42 }
42 }
43
43
44 safe_attributes 'name',
44 safe_attributes 'name',
45 'description',
45 'description',
46 'effective_date',
46 'effective_date',
47 'due_date',
47 'due_date',
48 'wiki_page_title',
48 'wiki_page_title',
49 'status',
49 'status',
50 'sharing',
50 'sharing',
51 'custom_field_values'
51 'custom_field_values'
52
52
53 # Returns true if +user+ or current user is allowed to view the version
53 # Returns true if +user+ or current user is allowed to view the version
54 def visible?(user=User.current)
54 def visible?(user=User.current)
55 user.allowed_to?(:view_issues, self.project)
55 user.allowed_to?(:view_issues, self.project)
56 end
56 end
57
57
58 # Version files have same visibility as project files
58 # Version files have same visibility as project files
59 def attachments_visible?(*args)
59 def attachments_visible?(*args)
60 project.present? && project.attachments_visible?(*args)
60 project.present? && project.attachments_visible?(*args)
61 end
61 end
62
62
63 def start_date
63 def start_date
64 @start_date ||= fixed_issues.minimum('start_date')
64 @start_date ||= fixed_issues.minimum('start_date')
65 end
65 end
66
66
67 def due_date
67 def due_date
68 effective_date
68 effective_date
69 end
69 end
70
70
71 def due_date=(arg)
71 def due_date=(arg)
72 self.effective_date=(arg)
72 self.effective_date=(arg)
73 end
73 end
74
74
75 # Returns the total estimated time for this version
75 # Returns the total estimated time for this version
76 # (sum of leaves estimated_hours)
76 # (sum of leaves estimated_hours)
77 def estimated_hours
77 def estimated_hours
78 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
78 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
79 end
79 end
80
80
81 # Returns the total reported time for this version
81 # Returns the total reported time for this version
82 def spent_hours
82 def spent_hours
83 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
83 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
84 end
84 end
85
85
86 def closed?
86 def closed?
87 status == 'closed'
87 status == 'closed'
88 end
88 end
89
89
90 def open?
90 def open?
91 status == 'open'
91 status == 'open'
92 end
92 end
93
93
94 # Returns true if the version is completed: due date reached and no open issues
94 # Returns true if the version is completed: due date reached and no open issues
95 def completed?
95 def completed?
96 effective_date && (effective_date < Date.today) && (open_issues_count == 0)
96 effective_date && (effective_date < Date.today) && (open_issues_count == 0)
97 end
97 end
98
98
99 def behind_schedule?
99 def behind_schedule?
100 if completed_pourcent == 100
100 if completed_pourcent == 100
101 return false
101 return false
102 elsif due_date && start_date
102 elsif due_date && start_date
103 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
103 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
104 return done_date <= Date.today
104 return done_date <= Date.today
105 else
105 else
106 false # No issues so it's not late
106 false # No issues so it's not late
107 end
107 end
108 end
108 end
109
109
110 # Returns the completion percentage of this version based on the amount of open/closed issues
110 # Returns the completion percentage of this version based on the amount of open/closed issues
111 # and the time spent on the open issues.
111 # and the time spent on the open issues.
112 def completed_pourcent
112 def completed_pourcent
113 if issues_count == 0
113 if issues_count == 0
114 0
114 0
115 elsif open_issues_count == 0
115 elsif open_issues_count == 0
116 100
116 100
117 else
117 else
118 issues_progress(false) + issues_progress(true)
118 issues_progress(false) + issues_progress(true)
119 end
119 end
120 end
120 end
121
121
122 # Returns the percentage of issues that have been marked as 'closed'.
122 # Returns the percentage of issues that have been marked as 'closed'.
123 def closed_pourcent
123 def closed_pourcent
124 if issues_count == 0
124 if issues_count == 0
125 0
125 0
126 else
126 else
127 issues_progress(false)
127 issues_progress(false)
128 end
128 end
129 end
129 end
130
130
131 # Returns true if the version is overdue: due date reached and some open issues
131 # Returns true if the version is overdue: due date reached and some open issues
132 def overdue?
132 def overdue?
133 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
133 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
134 end
134 end
135
135
136 # Returns assigned issues count
136 # Returns assigned issues count
137 def issues_count
137 def issues_count
138 load_issue_counts
138 load_issue_counts
139 @issue_count
139 @issue_count
140 end
140 end
141
141
142 # Returns the total amount of open issues for this version.
142 # Returns the total amount of open issues for this version.
143 def open_issues_count
143 def open_issues_count
144 load_issue_counts
144 load_issue_counts
145 @open_issues_count
145 @open_issues_count
146 end
146 end
147
147
148 # Returns the total amount of closed issues for this version.
148 # Returns the total amount of closed issues for this version.
149 def closed_issues_count
149 def closed_issues_count
150 load_issue_counts
150 load_issue_counts
151 @closed_issues_count
151 @closed_issues_count
152 end
152 end
153
153
154 def wiki_page
154 def wiki_page
155 if project.wiki && !wiki_page_title.blank?
155 if project.wiki && !wiki_page_title.blank?
156 @wiki_page ||= project.wiki.find_page(wiki_page_title)
156 @wiki_page ||= project.wiki.find_page(wiki_page_title)
157 end
157 end
158 @wiki_page
158 @wiki_page
159 end
159 end
160
160
161 def to_s; name end
161 def to_s; name end
162
162
163 def to_s_with_project
163 def to_s_with_project
164 "#{project} - #{name}"
164 "#{project} - #{name}"
165 end
165 end
166
166
167 # Versions are sorted by effective_date and name
167 # Versions are sorted by effective_date and name
168 # Those with no effective_date are at the end, sorted by name
168 # Those with no effective_date are at the end, sorted by name
169 def <=>(version)
169 def <=>(version)
170 if self.effective_date
170 if self.effective_date
171 if version.effective_date
171 if version.effective_date
172 if self.effective_date == version.effective_date
172 if self.effective_date == version.effective_date
173 name == version.name ? id <=> version.id : name <=> version.name
173 name == version.name ? id <=> version.id : name <=> version.name
174 else
174 else
175 self.effective_date <=> version.effective_date
175 self.effective_date <=> version.effective_date
176 end
176 end
177 else
177 else
178 -1
178 -1
179 end
179 end
180 else
180 else
181 if version.effective_date
181 if version.effective_date
182 1
182 1
183 else
183 else
184 name == version.name ? id <=> version.id : name <=> version.name
184 name == version.name ? id <=> version.id : name <=> version.name
185 end
185 end
186 end
186 end
187 end
187 end
188
188
189 def self.fields_for_order_statement(table=nil)
189 def self.fields_for_order_statement(table=nil)
190 table ||= table_name
190 table ||= table_name
191 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
191 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
192 end
192 end
193
193
194 scope :sorted, order(fields_for_order_statement)
194 scope :sorted, order(fields_for_order_statement)
195
195
196 # Returns the sharings that +user+ can set the version to
196 # Returns the sharings that +user+ can set the version to
197 def allowed_sharings(user = User.current)
197 def allowed_sharings(user = User.current)
198 VERSION_SHARINGS.select do |s|
198 VERSION_SHARINGS.select do |s|
199 if sharing == s
199 if sharing == s
200 true
200 true
201 else
201 else
202 case s
202 case s
203 when 'system'
203 when 'system'
204 # Only admin users can set a systemwide sharing
204 # Only admin users can set a systemwide sharing
205 user.admin?
205 user.admin?
206 when 'hierarchy', 'tree'
206 when 'hierarchy', 'tree'
207 # Only users allowed to manage versions of the root project can
207 # Only users allowed to manage versions of the root project can
208 # set sharing to hierarchy or tree
208 # set sharing to hierarchy or tree
209 project.nil? || user.allowed_to?(:manage_versions, project.root)
209 project.nil? || user.allowed_to?(:manage_versions, project.root)
210 else
210 else
211 true
211 true
212 end
212 end
213 end
213 end
214 end
214 end
215 end
215 end
216
216
217 private
217 private
218
218
219 def load_issue_counts
219 def load_issue_counts
220 unless @issue_count
220 unless @issue_count
221 @open_issues_count = 0
221 @open_issues_count = 0
222 @closed_issues_count = 0
222 @closed_issues_count = 0
223 fixed_issues.count(:all, :group => :status).each do |status, count|
223 fixed_issues.count(:all, :group => :status).each do |status, count|
224 if status.is_closed?
224 if status.is_closed?
225 @closed_issues_count += count
225 @closed_issues_count += count
226 else
226 else
227 @open_issues_count += count
227 @open_issues_count += count
228 end
228 end
229 end
229 end
230 @issue_count = @open_issues_count + @closed_issues_count
230 @issue_count = @open_issues_count + @closed_issues_count
231 end
231 end
232 end
232 end
233
233
234 # Update the issue's fixed versions. Used if a version's sharing changes.
234 # Update the issue's fixed versions. Used if a version's sharing changes.
235 def update_issues_from_sharing_change
235 def update_issues_from_sharing_change
236 if sharing_changed?
236 if sharing_changed?
237 if VERSION_SHARINGS.index(sharing_was).nil? ||
237 if VERSION_SHARINGS.index(sharing_was).nil? ||
238 VERSION_SHARINGS.index(sharing).nil? ||
238 VERSION_SHARINGS.index(sharing).nil? ||
239 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
239 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
240 Issue.update_versions_from_sharing_change self
240 Issue.update_versions_from_sharing_change self
241 end
241 end
242 end
242 end
243 end
243 end
244
244
245 # Returns the average estimated time of assigned issues
245 # Returns the average estimated time of assigned issues
246 # or 1 if no issue has an estimated time
246 # or 1 if no issue has an estimated time
247 # Used to weigth unestimated issues in progress calculation
247 # Used to weigth unestimated issues in progress calculation
248 def estimated_average
248 def estimated_average
249 if @estimated_average.nil?
249 if @estimated_average.nil?
250 average = fixed_issues.average(:estimated_hours).to_f
250 average = fixed_issues.average(:estimated_hours).to_f
251 if average == 0
251 if average == 0
252 average = 1
252 average = 1
253 end
253 end
254 @estimated_average = average
254 @estimated_average = average
255 end
255 end
256 @estimated_average
256 @estimated_average
257 end
257 end
258
258
259 # Returns the total progress of open or closed issues. The returned percentage takes into account
259 # Returns the total progress of open or closed issues. The returned percentage takes into account
260 # the amount of estimated time set for this version.
260 # the amount of estimated time set for this version.
261 #
261 #
262 # Examples:
262 # Examples:
263 # issues_progress(true) => returns the progress percentage for open issues.
263 # issues_progress(true) => returns the progress percentage for open issues.
264 # issues_progress(false) => returns the progress percentage for closed issues.
264 # issues_progress(false) => returns the progress percentage for closed issues.
265 def issues_progress(open)
265 def issues_progress(open)
266 @issues_progress ||= {}
266 @issues_progress ||= {}
267 @issues_progress[open] ||= begin
267 @issues_progress[open] ||= begin
268 progress = 0
268 progress = 0
269 if issues_count > 0
269 if issues_count > 0
270 ratio = open ? 'done_ratio' : 100
270 ratio = open ? 'done_ratio' : 100
271
271
272 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
272 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
273 progress = done / (estimated_average * issues_count)
273 progress = done / (estimated_average * issues_count)
274 end
274 end
275 progress
275 progress
276 end
276 end
277 end
277 end
278
278
279 def validate_version
279 def validate_version
280 if effective_date.nil? && @attributes['effective_date'].present?
280 if effective_date.nil? && @attributes['effective_date'].present?
281 errors.add :effective_date, :not_a_date
281 errors.add :effective_date, :not_a_date
282 end
282 end
283 end
283 end
284 end
284 end
@@ -1,97 +1,97
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Wiki < ActiveRecord::Base
18 class Wiki < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 belongs_to :project
20 belongs_to :project
21 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
21 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
22 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
22 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
23
23
24 acts_as_watchable
24 acts_as_watchable
25
25
26 validates_presence_of :start_page
26 validates_presence_of :start_page
27 validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
27 validates_format_of :start_page, :with => /\A[^,\.\/\?\;\|\:]*\z/
28
28
29 safe_attributes 'start_page'
29 safe_attributes 'start_page'
30
30
31 def visible?(user=User.current)
31 def visible?(user=User.current)
32 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
32 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
33 end
33 end
34
34
35 # Returns the wiki page that acts as the sidebar content
35 # Returns the wiki page that acts as the sidebar content
36 # or nil if no such page exists
36 # or nil if no such page exists
37 def sidebar
37 def sidebar
38 @sidebar ||= find_page('Sidebar', :with_redirect => false)
38 @sidebar ||= find_page('Sidebar', :with_redirect => false)
39 end
39 end
40
40
41 # find the page with the given title
41 # find the page with the given title
42 # if page doesn't exist, return a new page
42 # if page doesn't exist, return a new page
43 def find_or_new_page(title)
43 def find_or_new_page(title)
44 title = start_page if title.blank?
44 title = start_page if title.blank?
45 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
45 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
46 end
46 end
47
47
48 # find the page with the given title
48 # find the page with the given title
49 def find_page(title, options = {})
49 def find_page(title, options = {})
50 @page_found_with_redirect = false
50 @page_found_with_redirect = false
51 title = start_page if title.blank?
51 title = start_page if title.blank?
52 title = Wiki.titleize(title)
52 title = Wiki.titleize(title)
53 page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
53 page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
54 if !page && !(options[:with_redirect] == false)
54 if !page && !(options[:with_redirect] == false)
55 # search for a redirect
55 # search for a redirect
56 redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
56 redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
57 if redirect
57 if redirect
58 page = find_page(redirect.redirects_to, :with_redirect => false)
58 page = find_page(redirect.redirects_to, :with_redirect => false)
59 @page_found_with_redirect = true
59 @page_found_with_redirect = true
60 end
60 end
61 end
61 end
62 page
62 page
63 end
63 end
64
64
65 # Returns true if the last page was found with a redirect
65 # Returns true if the last page was found with a redirect
66 def page_found_with_redirect?
66 def page_found_with_redirect?
67 @page_found_with_redirect
67 @page_found_with_redirect
68 end
68 end
69
69
70 # Finds a page by title
70 # Finds a page by title
71 # The given string can be of one of the forms: "title" or "project:title"
71 # The given string can be of one of the forms: "title" or "project:title"
72 # Examples:
72 # Examples:
73 # Wiki.find_page("bar", project => foo)
73 # Wiki.find_page("bar", project => foo)
74 # Wiki.find_page("foo:bar")
74 # Wiki.find_page("foo:bar")
75 def self.find_page(title, options = {})
75 def self.find_page(title, options = {})
76 project = options[:project]
76 project = options[:project]
77 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
77 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
78 project_identifier, title = $1, $2
78 project_identifier, title = $1, $2
79 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
79 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
80 end
80 end
81 if project && project.wiki
81 if project && project.wiki
82 page = project.wiki.find_page(title)
82 page = project.wiki.find_page(title)
83 if page && page.content
83 if page && page.content
84 page
84 page
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 # turn a string into a valid page title
89 # turn a string into a valid page title
90 def self.titleize(title)
90 def self.titleize(title)
91 # replace spaces with _ and remove unwanted caracters
91 # replace spaces with _ and remove unwanted caracters
92 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
92 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
93 # upcase the first letter
93 # upcase the first letter
94 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
94 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
95 title
95 title
96 end
96 end
97 end
97 end
General Comments 0
You need to be logged in to leave comments. Login now