##// END OF EJS Templates
Fixed "can't convert Fixnum into String" error on projects with numerical identifier (#10135)....
Jean-Philippe Lang -
r8684:dfbab5d61ee7
parent child
Show More
@@ -1,900 +1,900
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :conditions => ["is_default = ?", true]
49 has_one :repository, :conditions => ["is_default = ?", true]
50 has_many :repositories, :dependent => :destroy
50 has_many :repositories, :dependent => :destroy
51 has_many :changesets, :through => :repository
51 has_many :changesets, :through => :repository
52 has_one :wiki, :dependent => :destroy
52 has_one :wiki, :dependent => :destroy
53 # Custom field for the project issues
53 # Custom field for the project issues
54 has_and_belongs_to_many :issue_custom_fields,
54 has_and_belongs_to_many :issue_custom_fields,
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :order => "#{CustomField.table_name}.position",
56 :order => "#{CustomField.table_name}.position",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :association_foreign_key => 'custom_field_id'
58 :association_foreign_key => 'custom_field_id'
59
59
60 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_attachable :view_permission => :view_files,
61 acts_as_attachable :view_permission => :view_files,
62 :delete_permission => :manage_files
62 :delete_permission => :manage_files
63
63
64 acts_as_customizable
64 acts_as_customizable
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :author => nil
68 :author => nil
69
69
70 attr_protected :status
70 attr_protected :status
71
71
72 validates_presence_of :name, :identifier
72 validates_presence_of :name, :identifier
73 validates_uniqueness_of :identifier
73 validates_uniqueness_of :identifier
74 validates_associated :repository, :wiki
74 validates_associated :repository, :wiki
75 validates_length_of :name, :maximum => 255
75 validates_length_of :name, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 # donwcase letters, digits, dashes but not digits only
78 # donwcase letters, digits, dashes but not digits only
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 # reserved words
80 # reserved words
81 validates_exclusion_of :identifier, :in => %w( new )
81 validates_exclusion_of :identifier, :in => %w( new )
82
82
83 before_destroy :delete_all_members
83 before_destroy :delete_all_members
84
84
85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
88 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :all_public, { :conditions => { :is_public => true } }
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
90 named_scope :allowed_to, lambda {|*args|
90 named_scope :allowed_to, lambda {|*args|
91 user = User.current
91 user = User.current
92 permission = nil
92 permission = nil
93 if args.first.is_a?(Symbol)
93 if args.first.is_a?(Symbol)
94 permission = args.shift
94 permission = args.shift
95 else
95 else
96 user = args.shift
96 user = args.shift
97 permission = args.shift
97 permission = args.shift
98 end
98 end
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
100 }
100 }
101 named_scope :like, lambda {|arg|
101 named_scope :like, lambda {|arg|
102 if arg.blank?
102 if arg.blank?
103 {}
103 {}
104 else
104 else
105 pattern = "%#{arg.to_s.strip.downcase}%"
105 pattern = "%#{arg.to_s.strip.downcase}%"
106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
107 end
107 end
108 }
108 }
109
109
110 def initialize(attributes=nil, *args)
110 def initialize(attributes=nil, *args)
111 super
111 super
112
112
113 initialized = (attributes || {}).stringify_keys
113 initialized = (attributes || {}).stringify_keys
114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
115 self.identifier = Project.next_identifier
115 self.identifier = Project.next_identifier
116 end
116 end
117 if !initialized.key?('is_public')
117 if !initialized.key?('is_public')
118 self.is_public = Setting.default_projects_public?
118 self.is_public = Setting.default_projects_public?
119 end
119 end
120 if !initialized.key?('enabled_module_names')
120 if !initialized.key?('enabled_module_names')
121 self.enabled_module_names = Setting.default_projects_modules
121 self.enabled_module_names = Setting.default_projects_modules
122 end
122 end
123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
124 self.trackers = Tracker.all
124 self.trackers = Tracker.all
125 end
125 end
126 end
126 end
127
127
128 def identifier=(identifier)
128 def identifier=(identifier)
129 super unless identifier_frozen?
129 super unless identifier_frozen?
130 end
130 end
131
131
132 def identifier_frozen?
132 def identifier_frozen?
133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
134 end
134 end
135
135
136 # returns latest created projects
136 # returns latest created projects
137 # non public projects will be returned only if user is a member of those
137 # non public projects will be returned only if user is a member of those
138 def self.latest(user=nil, count=5)
138 def self.latest(user=nil, count=5)
139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
140 end
140 end
141
141
142 # Returns true if the project is visible to +user+ or to the current user.
142 # Returns true if the project is visible to +user+ or to the current user.
143 def visible?(user=User.current)
143 def visible?(user=User.current)
144 user.allowed_to?(:view_project, self)
144 user.allowed_to?(:view_project, self)
145 end
145 end
146
146
147 # Returns a SQL conditions string used to find all projects visible by the specified user.
147 # Returns a SQL conditions string used to find all projects visible by the specified user.
148 #
148 #
149 # Examples:
149 # Examples:
150 # Project.visible_condition(admin) => "projects.status = 1"
150 # Project.visible_condition(admin) => "projects.status = 1"
151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
153 def self.visible_condition(user, options={})
153 def self.visible_condition(user, options={})
154 allowed_to_condition(user, :view_project, options)
154 allowed_to_condition(user, :view_project, options)
155 end
155 end
156
156
157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
158 #
158 #
159 # Valid options:
159 # Valid options:
160 # * :project => limit the condition to project
160 # * :project => limit the condition to project
161 # * :with_subprojects => limit the condition to project and its subprojects
161 # * :with_subprojects => limit the condition to project and its subprojects
162 # * :member => limit the condition to the user projects
162 # * :member => limit the condition to the user projects
163 def self.allowed_to_condition(user, permission, options={})
163 def self.allowed_to_condition(user, permission, options={})
164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
165 if perm = Redmine::AccessControl.permission(permission)
165 if perm = Redmine::AccessControl.permission(permission)
166 unless perm.project_module.nil?
166 unless perm.project_module.nil?
167 # If the permission belongs to a project module, make sure the module is enabled
167 # If the permission belongs to a project module, make sure the module is enabled
168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
169 end
169 end
170 end
170 end
171 if options[:project]
171 if options[:project]
172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
174 base_statement = "(#{project_statement}) AND (#{base_statement})"
174 base_statement = "(#{project_statement}) AND (#{base_statement})"
175 end
175 end
176
176
177 if user.admin?
177 if user.admin?
178 base_statement
178 base_statement
179 else
179 else
180 statement_by_role = {}
180 statement_by_role = {}
181 unless options[:member]
181 unless options[:member]
182 role = user.logged? ? Role.non_member : Role.anonymous
182 role = user.logged? ? Role.non_member : Role.anonymous
183 if role.allowed_to?(permission)
183 if role.allowed_to?(permission)
184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
185 end
185 end
186 end
186 end
187 if user.logged?
187 if user.logged?
188 user.projects_by_role.each do |role, projects|
188 user.projects_by_role.each do |role, projects|
189 if role.allowed_to?(permission)
189 if role.allowed_to?(permission)
190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
191 end
191 end
192 end
192 end
193 end
193 end
194 if statement_by_role.empty?
194 if statement_by_role.empty?
195 "1=0"
195 "1=0"
196 else
196 else
197 if block_given?
197 if block_given?
198 statement_by_role.each do |role, statement|
198 statement_by_role.each do |role, statement|
199 if s = yield(role, user)
199 if s = yield(role, user)
200 statement_by_role[role] = "(#{statement} AND (#{s}))"
200 statement_by_role[role] = "(#{statement} AND (#{s}))"
201 end
201 end
202 end
202 end
203 end
203 end
204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 # Returns the Systemwide and project specific activities
209 # Returns the Systemwide and project specific activities
210 def activities(include_inactive=false)
210 def activities(include_inactive=false)
211 if include_inactive
211 if include_inactive
212 return all_activities
212 return all_activities
213 else
213 else
214 return active_activities
214 return active_activities
215 end
215 end
216 end
216 end
217
217
218 # Will create a new Project specific Activity or update an existing one
218 # Will create a new Project specific Activity or update an existing one
219 #
219 #
220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
221 # does not successfully save.
221 # does not successfully save.
222 def update_or_create_time_entry_activity(id, activity_hash)
222 def update_or_create_time_entry_activity(id, activity_hash)
223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
224 self.create_time_entry_activity_if_needed(activity_hash)
224 self.create_time_entry_activity_if_needed(activity_hash)
225 else
225 else
226 activity = project.time_entry_activities.find_by_id(id.to_i)
226 activity = project.time_entry_activities.find_by_id(id.to_i)
227 activity.update_attributes(activity_hash) if activity
227 activity.update_attributes(activity_hash) if activity
228 end
228 end
229 end
229 end
230
230
231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
232 #
232 #
233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
234 # does not successfully save.
234 # does not successfully save.
235 def create_time_entry_activity_if_needed(activity)
235 def create_time_entry_activity_if_needed(activity)
236 if activity['parent_id']
236 if activity['parent_id']
237
237
238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
239 activity['name'] = parent_activity.name
239 activity['name'] = parent_activity.name
240 activity['position'] = parent_activity.position
240 activity['position'] = parent_activity.position
241
241
242 if Enumeration.overridding_change?(activity, parent_activity)
242 if Enumeration.overridding_change?(activity, parent_activity)
243 project_activity = self.time_entry_activities.create(activity)
243 project_activity = self.time_entry_activities.create(activity)
244
244
245 if project_activity.new_record?
245 if project_activity.new_record?
246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
247 else
247 else
248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
249 end
249 end
250 end
250 end
251 end
251 end
252 end
252 end
253
253
254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
255 #
255 #
256 # Examples:
256 # Examples:
257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
258 # project.project_condition(false) => "projects.id = 1"
258 # project.project_condition(false) => "projects.id = 1"
259 def project_condition(with_subprojects)
259 def project_condition(with_subprojects)
260 cond = "#{Project.table_name}.id = #{id}"
260 cond = "#{Project.table_name}.id = #{id}"
261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
262 cond
262 cond
263 end
263 end
264
264
265 def self.find(*args)
265 def self.find(*args)
266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
267 project = find_by_identifier(*args)
267 project = find_by_identifier(*args)
268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
269 project
269 project
270 else
270 else
271 super
271 super
272 end
272 end
273 end
273 end
274
274
275 def to_param
275 def to_param
276 # id is used for projects with a numeric identifier (compatibility)
276 # id is used for projects with a numeric identifier (compatibility)
277 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
277 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
278 end
278 end
279
279
280 def active?
280 def active?
281 self.status == STATUS_ACTIVE
281 self.status == STATUS_ACTIVE
282 end
282 end
283
283
284 def archived?
284 def archived?
285 self.status == STATUS_ARCHIVED
285 self.status == STATUS_ARCHIVED
286 end
286 end
287
287
288 # Archives the project and its descendants
288 # Archives the project and its descendants
289 def archive
289 def archive
290 # Check that there is no issue of a non descendant project that is assigned
290 # Check that there is no issue of a non descendant project that is assigned
291 # to one of the project or descendant versions
291 # to one of the project or descendant versions
292 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
292 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
293 if v_ids.any? && Issue.find(:first, :include => :project,
293 if v_ids.any? && Issue.find(:first, :include => :project,
294 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
294 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
295 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
295 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
296 return false
296 return false
297 end
297 end
298 Project.transaction do
298 Project.transaction do
299 archive!
299 archive!
300 end
300 end
301 true
301 true
302 end
302 end
303
303
304 # Unarchives the project
304 # Unarchives the project
305 # All its ancestors must be active
305 # All its ancestors must be active
306 def unarchive
306 def unarchive
307 return false if ancestors.detect {|a| !a.active?}
307 return false if ancestors.detect {|a| !a.active?}
308 update_attribute :status, STATUS_ACTIVE
308 update_attribute :status, STATUS_ACTIVE
309 end
309 end
310
310
311 # Returns an array of projects the project can be moved to
311 # Returns an array of projects the project can be moved to
312 # by the current user
312 # by the current user
313 def allowed_parents
313 def allowed_parents
314 return @allowed_parents if @allowed_parents
314 return @allowed_parents if @allowed_parents
315 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
315 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
316 @allowed_parents = @allowed_parents - self_and_descendants
316 @allowed_parents = @allowed_parents - self_and_descendants
317 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
317 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
318 @allowed_parents << nil
318 @allowed_parents << nil
319 end
319 end
320 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
320 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
321 @allowed_parents << parent
321 @allowed_parents << parent
322 end
322 end
323 @allowed_parents
323 @allowed_parents
324 end
324 end
325
325
326 # Sets the parent of the project with authorization check
326 # Sets the parent of the project with authorization check
327 def set_allowed_parent!(p)
327 def set_allowed_parent!(p)
328 unless p.nil? || p.is_a?(Project)
328 unless p.nil? || p.is_a?(Project)
329 if p.to_s.blank?
329 if p.to_s.blank?
330 p = nil
330 p = nil
331 else
331 else
332 p = Project.find_by_id(p)
332 p = Project.find_by_id(p)
333 return false unless p
333 return false unless p
334 end
334 end
335 end
335 end
336 if p.nil?
336 if p.nil?
337 if !new_record? && allowed_parents.empty?
337 if !new_record? && allowed_parents.empty?
338 return false
338 return false
339 end
339 end
340 elsif !allowed_parents.include?(p)
340 elsif !allowed_parents.include?(p)
341 return false
341 return false
342 end
342 end
343 set_parent!(p)
343 set_parent!(p)
344 end
344 end
345
345
346 # Sets the parent of the project
346 # Sets the parent of the project
347 # Argument can be either a Project, a String, a Fixnum or nil
347 # Argument can be either a Project, a String, a Fixnum or nil
348 def set_parent!(p)
348 def set_parent!(p)
349 unless p.nil? || p.is_a?(Project)
349 unless p.nil? || p.is_a?(Project)
350 if p.to_s.blank?
350 if p.to_s.blank?
351 p = nil
351 p = nil
352 else
352 else
353 p = Project.find_by_id(p)
353 p = Project.find_by_id(p)
354 return false unless p
354 return false unless p
355 end
355 end
356 end
356 end
357 if p == parent && !p.nil?
357 if p == parent && !p.nil?
358 # Nothing to do
358 # Nothing to do
359 true
359 true
360 elsif p.nil? || (p.active? && move_possible?(p))
360 elsif p.nil? || (p.active? && move_possible?(p))
361 # Insert the project so that target's children or root projects stay alphabetically sorted
361 # Insert the project so that target's children or root projects stay alphabetically sorted
362 sibs = (p.nil? ? self.class.roots : p.children)
362 sibs = (p.nil? ? self.class.roots : p.children)
363 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
363 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
364 if to_be_inserted_before
364 if to_be_inserted_before
365 move_to_left_of(to_be_inserted_before)
365 move_to_left_of(to_be_inserted_before)
366 elsif p.nil?
366 elsif p.nil?
367 if sibs.empty?
367 if sibs.empty?
368 # move_to_root adds the project in first (ie. left) position
368 # move_to_root adds the project in first (ie. left) position
369 move_to_root
369 move_to_root
370 else
370 else
371 move_to_right_of(sibs.last) unless self == sibs.last
371 move_to_right_of(sibs.last) unless self == sibs.last
372 end
372 end
373 else
373 else
374 # move_to_child_of adds the project in last (ie.right) position
374 # move_to_child_of adds the project in last (ie.right) position
375 move_to_child_of(p)
375 move_to_child_of(p)
376 end
376 end
377 Issue.update_versions_from_hierarchy_change(self)
377 Issue.update_versions_from_hierarchy_change(self)
378 true
378 true
379 else
379 else
380 # Can not move to the given target
380 # Can not move to the given target
381 false
381 false
382 end
382 end
383 end
383 end
384
384
385 # Returns an array of the trackers used by the project and its active sub projects
385 # Returns an array of the trackers used by the project and its active sub projects
386 def rolled_up_trackers
386 def rolled_up_trackers
387 @rolled_up_trackers ||=
387 @rolled_up_trackers ||=
388 Tracker.find(:all, :joins => :projects,
388 Tracker.find(:all, :joins => :projects,
389 :select => "DISTINCT #{Tracker.table_name}.*",
389 :select => "DISTINCT #{Tracker.table_name}.*",
390 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
390 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
391 :order => "#{Tracker.table_name}.position")
391 :order => "#{Tracker.table_name}.position")
392 end
392 end
393
393
394 # Closes open and locked project versions that are completed
394 # Closes open and locked project versions that are completed
395 def close_completed_versions
395 def close_completed_versions
396 Version.transaction do
396 Version.transaction do
397 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
397 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
398 if version.completed?
398 if version.completed?
399 version.update_attribute(:status, 'closed')
399 version.update_attribute(:status, 'closed')
400 end
400 end
401 end
401 end
402 end
402 end
403 end
403 end
404
404
405 # Returns a scope of the Versions on subprojects
405 # Returns a scope of the Versions on subprojects
406 def rolled_up_versions
406 def rolled_up_versions
407 @rolled_up_versions ||=
407 @rolled_up_versions ||=
408 Version.scoped(:include => :project,
408 Version.scoped(:include => :project,
409 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
409 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
410 end
410 end
411
411
412 # Returns a scope of the Versions used by the project
412 # Returns a scope of the Versions used by the project
413 def shared_versions
413 def shared_versions
414 @shared_versions ||= begin
414 @shared_versions ||= begin
415 r = root? ? self : root
415 r = root? ? self : root
416 Version.scoped(:include => :project,
416 Version.scoped(:include => :project,
417 :conditions => "#{Project.table_name}.id = #{id}" +
417 :conditions => "#{Project.table_name}.id = #{id}" +
418 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
418 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
419 " #{Version.table_name}.sharing = 'system'" +
419 " #{Version.table_name}.sharing = 'system'" +
420 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
420 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
421 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
421 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
422 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
422 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
423 "))")
423 "))")
424 end
424 end
425 end
425 end
426
426
427 # Returns a hash of project users grouped by role
427 # Returns a hash of project users grouped by role
428 def users_by_role
428 def users_by_role
429 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
429 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
430 m.roles.each do |r|
430 m.roles.each do |r|
431 h[r] ||= []
431 h[r] ||= []
432 h[r] << m.user
432 h[r] << m.user
433 end
433 end
434 h
434 h
435 end
435 end
436 end
436 end
437
437
438 # Deletes all project's members
438 # Deletes all project's members
439 def delete_all_members
439 def delete_all_members
440 me, mr = Member.table_name, MemberRole.table_name
440 me, mr = Member.table_name, MemberRole.table_name
441 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
441 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
442 Member.delete_all(['project_id = ?', id])
442 Member.delete_all(['project_id = ?', id])
443 end
443 end
444
444
445 # Users/groups issues can be assigned to
445 # Users/groups issues can be assigned to
446 def assignable_users
446 def assignable_users
447 assignable = Setting.issue_group_assignment? ? member_principals : members
447 assignable = Setting.issue_group_assignment? ? member_principals : members
448 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
448 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
449 end
449 end
450
450
451 # Returns the mail adresses of users that should be always notified on project events
451 # Returns the mail adresses of users that should be always notified on project events
452 def recipients
452 def recipients
453 notified_users.collect {|user| user.mail}
453 notified_users.collect {|user| user.mail}
454 end
454 end
455
455
456 # Returns the users that should be notified on project events
456 # Returns the users that should be notified on project events
457 def notified_users
457 def notified_users
458 # TODO: User part should be extracted to User#notify_about?
458 # TODO: User part should be extracted to User#notify_about?
459 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
459 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
460 end
460 end
461
461
462 # Returns an array of all custom fields enabled for project issues
462 # Returns an array of all custom fields enabled for project issues
463 # (explictly associated custom fields and custom fields enabled for all projects)
463 # (explictly associated custom fields and custom fields enabled for all projects)
464 def all_issue_custom_fields
464 def all_issue_custom_fields
465 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
465 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
466 end
466 end
467
467
468 # Returns an array of all custom fields enabled for project time entries
468 # Returns an array of all custom fields enabled for project time entries
469 # (explictly associated custom fields and custom fields enabled for all projects)
469 # (explictly associated custom fields and custom fields enabled for all projects)
470 def all_time_entry_custom_fields
470 def all_time_entry_custom_fields
471 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
471 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
472 end
472 end
473
473
474 def project
474 def project
475 self
475 self
476 end
476 end
477
477
478 def <=>(project)
478 def <=>(project)
479 name.downcase <=> project.name.downcase
479 name.downcase <=> project.name.downcase
480 end
480 end
481
481
482 def to_s
482 def to_s
483 name
483 name
484 end
484 end
485
485
486 # Returns a short description of the projects (first lines)
486 # Returns a short description of the projects (first lines)
487 def short_description(length = 255)
487 def short_description(length = 255)
488 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
488 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
489 end
489 end
490
490
491 def css_classes
491 def css_classes
492 s = 'project'
492 s = 'project'
493 s << ' root' if root?
493 s << ' root' if root?
494 s << ' child' if child?
494 s << ' child' if child?
495 s << (leaf? ? ' leaf' : ' parent')
495 s << (leaf? ? ' leaf' : ' parent')
496 s
496 s
497 end
497 end
498
498
499 # The earliest start date of a project, based on it's issues and versions
499 # The earliest start date of a project, based on it's issues and versions
500 def start_date
500 def start_date
501 [
501 [
502 issues.minimum('start_date'),
502 issues.minimum('start_date'),
503 shared_versions.collect(&:effective_date),
503 shared_versions.collect(&:effective_date),
504 shared_versions.collect(&:start_date)
504 shared_versions.collect(&:start_date)
505 ].flatten.compact.min
505 ].flatten.compact.min
506 end
506 end
507
507
508 # The latest due date of an issue or version
508 # The latest due date of an issue or version
509 def due_date
509 def due_date
510 [
510 [
511 issues.maximum('due_date'),
511 issues.maximum('due_date'),
512 shared_versions.collect(&:effective_date),
512 shared_versions.collect(&:effective_date),
513 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
513 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
514 ].flatten.compact.max
514 ].flatten.compact.max
515 end
515 end
516
516
517 def overdue?
517 def overdue?
518 active? && !due_date.nil? && (due_date < Date.today)
518 active? && !due_date.nil? && (due_date < Date.today)
519 end
519 end
520
520
521 # Returns the percent completed for this project, based on the
521 # Returns the percent completed for this project, based on the
522 # progress on it's versions.
522 # progress on it's versions.
523 def completed_percent(options={:include_subprojects => false})
523 def completed_percent(options={:include_subprojects => false})
524 if options.delete(:include_subprojects)
524 if options.delete(:include_subprojects)
525 total = self_and_descendants.collect(&:completed_percent).sum
525 total = self_and_descendants.collect(&:completed_percent).sum
526
526
527 total / self_and_descendants.count
527 total / self_and_descendants.count
528 else
528 else
529 if versions.count > 0
529 if versions.count > 0
530 total = versions.collect(&:completed_pourcent).sum
530 total = versions.collect(&:completed_pourcent).sum
531
531
532 total / versions.count
532 total / versions.count
533 else
533 else
534 100
534 100
535 end
535 end
536 end
536 end
537 end
537 end
538
538
539 # Return true if this project is allowed to do the specified action.
539 # Return true if this project is allowed to do the specified action.
540 # action can be:
540 # action can be:
541 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
541 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
542 # * a permission Symbol (eg. :edit_project)
542 # * a permission Symbol (eg. :edit_project)
543 def allows_to?(action)
543 def allows_to?(action)
544 if action.is_a? Hash
544 if action.is_a? Hash
545 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
545 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
546 else
546 else
547 allowed_permissions.include? action
547 allowed_permissions.include? action
548 end
548 end
549 end
549 end
550
550
551 def module_enabled?(module_name)
551 def module_enabled?(module_name)
552 module_name = module_name.to_s
552 module_name = module_name.to_s
553 enabled_modules.detect {|m| m.name == module_name}
553 enabled_modules.detect {|m| m.name == module_name}
554 end
554 end
555
555
556 def enabled_module_names=(module_names)
556 def enabled_module_names=(module_names)
557 if module_names && module_names.is_a?(Array)
557 if module_names && module_names.is_a?(Array)
558 module_names = module_names.collect(&:to_s).reject(&:blank?)
558 module_names = module_names.collect(&:to_s).reject(&:blank?)
559 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
559 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
560 else
560 else
561 enabled_modules.clear
561 enabled_modules.clear
562 end
562 end
563 end
563 end
564
564
565 # Returns an array of the enabled modules names
565 # Returns an array of the enabled modules names
566 def enabled_module_names
566 def enabled_module_names
567 enabled_modules.collect(&:name)
567 enabled_modules.collect(&:name)
568 end
568 end
569
569
570 # Enable a specific module
570 # Enable a specific module
571 #
571 #
572 # Examples:
572 # Examples:
573 # project.enable_module!(:issue_tracking)
573 # project.enable_module!(:issue_tracking)
574 # project.enable_module!("issue_tracking")
574 # project.enable_module!("issue_tracking")
575 def enable_module!(name)
575 def enable_module!(name)
576 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
576 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
577 end
577 end
578
578
579 # Disable a module if it exists
579 # Disable a module if it exists
580 #
580 #
581 # Examples:
581 # Examples:
582 # project.disable_module!(:issue_tracking)
582 # project.disable_module!(:issue_tracking)
583 # project.disable_module!("issue_tracking")
583 # project.disable_module!("issue_tracking")
584 # project.disable_module!(project.enabled_modules.first)
584 # project.disable_module!(project.enabled_modules.first)
585 def disable_module!(target)
585 def disable_module!(target)
586 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
586 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
587 target.destroy unless target.blank?
587 target.destroy unless target.blank?
588 end
588 end
589
589
590 safe_attributes 'name',
590 safe_attributes 'name',
591 'description',
591 'description',
592 'homepage',
592 'homepage',
593 'is_public',
593 'is_public',
594 'identifier',
594 'identifier',
595 'custom_field_values',
595 'custom_field_values',
596 'custom_fields',
596 'custom_fields',
597 'tracker_ids',
597 'tracker_ids',
598 'issue_custom_field_ids'
598 'issue_custom_field_ids'
599
599
600 safe_attributes 'enabled_module_names',
600 safe_attributes 'enabled_module_names',
601 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
601 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
602
602
603 # Returns an array of projects that are in this project's hierarchy
603 # Returns an array of projects that are in this project's hierarchy
604 #
604 #
605 # Example: parents, children, siblings
605 # Example: parents, children, siblings
606 def hierarchy
606 def hierarchy
607 parents = project.self_and_ancestors || []
607 parents = project.self_and_ancestors || []
608 descendants = project.descendants || []
608 descendants = project.descendants || []
609 project_hierarchy = parents | descendants # Set union
609 project_hierarchy = parents | descendants # Set union
610 end
610 end
611
611
612 # Returns an auto-generated project identifier based on the last identifier used
612 # Returns an auto-generated project identifier based on the last identifier used
613 def self.next_identifier
613 def self.next_identifier
614 p = Project.find(:first, :order => 'created_on DESC')
614 p = Project.find(:first, :order => 'created_on DESC')
615 p.nil? ? nil : p.identifier.to_s.succ
615 p.nil? ? nil : p.identifier.to_s.succ
616 end
616 end
617
617
618 # Copies and saves the Project instance based on the +project+.
618 # Copies and saves the Project instance based on the +project+.
619 # Duplicates the source project's:
619 # Duplicates the source project's:
620 # * Wiki
620 # * Wiki
621 # * Versions
621 # * Versions
622 # * Categories
622 # * Categories
623 # * Issues
623 # * Issues
624 # * Members
624 # * Members
625 # * Queries
625 # * Queries
626 #
626 #
627 # Accepts an +options+ argument to specify what to copy
627 # Accepts an +options+ argument to specify what to copy
628 #
628 #
629 # Examples:
629 # Examples:
630 # project.copy(1) # => copies everything
630 # project.copy(1) # => copies everything
631 # project.copy(1, :only => 'members') # => copies members only
631 # project.copy(1, :only => 'members') # => copies members only
632 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
632 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
633 def copy(project, options={})
633 def copy(project, options={})
634 project = project.is_a?(Project) ? project : Project.find(project)
634 project = project.is_a?(Project) ? project : Project.find(project)
635
635
636 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
636 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
637 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
637 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
638
638
639 Project.transaction do
639 Project.transaction do
640 if save
640 if save
641 reload
641 reload
642 to_be_copied.each do |name|
642 to_be_copied.each do |name|
643 send "copy_#{name}", project
643 send "copy_#{name}", project
644 end
644 end
645 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
645 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
646 save
646 save
647 end
647 end
648 end
648 end
649 end
649 end
650
650
651
651
652 # Copies +project+ and returns the new instance. This will not save
652 # Copies +project+ and returns the new instance. This will not save
653 # the copy
653 # the copy
654 def self.copy_from(project)
654 def self.copy_from(project)
655 begin
655 begin
656 project = project.is_a?(Project) ? project : Project.find(project)
656 project = project.is_a?(Project) ? project : Project.find(project)
657 if project
657 if project
658 # clear unique attributes
658 # clear unique attributes
659 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
659 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
660 copy = Project.new(attributes)
660 copy = Project.new(attributes)
661 copy.enabled_modules = project.enabled_modules
661 copy.enabled_modules = project.enabled_modules
662 copy.trackers = project.trackers
662 copy.trackers = project.trackers
663 copy.custom_values = project.custom_values.collect {|v| v.clone}
663 copy.custom_values = project.custom_values.collect {|v| v.clone}
664 copy.issue_custom_fields = project.issue_custom_fields
664 copy.issue_custom_fields = project.issue_custom_fields
665 return copy
665 return copy
666 else
666 else
667 return nil
667 return nil
668 end
668 end
669 rescue ActiveRecord::RecordNotFound
669 rescue ActiveRecord::RecordNotFound
670 return nil
670 return nil
671 end
671 end
672 end
672 end
673
673
674 # Yields the given block for each project with its level in the tree
674 # Yields the given block for each project with its level in the tree
675 def self.project_tree(projects, &block)
675 def self.project_tree(projects, &block)
676 ancestors = []
676 ancestors = []
677 projects.sort_by(&:lft).each do |project|
677 projects.sort_by(&:lft).each do |project|
678 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
678 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
679 ancestors.pop
679 ancestors.pop
680 end
680 end
681 yield project, ancestors.size
681 yield project, ancestors.size
682 ancestors << project
682 ancestors << project
683 end
683 end
684 end
684 end
685
685
686 private
686 private
687
687
688 # Copies wiki from +project+
688 # Copies wiki from +project+
689 def copy_wiki(project)
689 def copy_wiki(project)
690 # Check that the source project has a wiki first
690 # Check that the source project has a wiki first
691 unless project.wiki.nil?
691 unless project.wiki.nil?
692 self.wiki ||= Wiki.new
692 self.wiki ||= Wiki.new
693 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
693 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
694 wiki_pages_map = {}
694 wiki_pages_map = {}
695 project.wiki.pages.each do |page|
695 project.wiki.pages.each do |page|
696 # Skip pages without content
696 # Skip pages without content
697 next if page.content.nil?
697 next if page.content.nil?
698 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
698 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
699 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
699 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
700 new_wiki_page.content = new_wiki_content
700 new_wiki_page.content = new_wiki_content
701 wiki.pages << new_wiki_page
701 wiki.pages << new_wiki_page
702 wiki_pages_map[page.id] = new_wiki_page
702 wiki_pages_map[page.id] = new_wiki_page
703 end
703 end
704 wiki.save
704 wiki.save
705 # Reproduce page hierarchy
705 # Reproduce page hierarchy
706 project.wiki.pages.each do |page|
706 project.wiki.pages.each do |page|
707 if page.parent_id && wiki_pages_map[page.id]
707 if page.parent_id && wiki_pages_map[page.id]
708 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
708 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
709 wiki_pages_map[page.id].save
709 wiki_pages_map[page.id].save
710 end
710 end
711 end
711 end
712 end
712 end
713 end
713 end
714
714
715 # Copies versions from +project+
715 # Copies versions from +project+
716 def copy_versions(project)
716 def copy_versions(project)
717 project.versions.each do |version|
717 project.versions.each do |version|
718 new_version = Version.new
718 new_version = Version.new
719 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
719 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
720 self.versions << new_version
720 self.versions << new_version
721 end
721 end
722 end
722 end
723
723
724 # Copies issue categories from +project+
724 # Copies issue categories from +project+
725 def copy_issue_categories(project)
725 def copy_issue_categories(project)
726 project.issue_categories.each do |issue_category|
726 project.issue_categories.each do |issue_category|
727 new_issue_category = IssueCategory.new
727 new_issue_category = IssueCategory.new
728 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
728 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
729 self.issue_categories << new_issue_category
729 self.issue_categories << new_issue_category
730 end
730 end
731 end
731 end
732
732
733 # Copies issues from +project+
733 # Copies issues from +project+
734 # Note: issues assigned to a closed version won't be copied due to validation rules
734 # Note: issues assigned to a closed version won't be copied due to validation rules
735 def copy_issues(project)
735 def copy_issues(project)
736 # Stores the source issue id as a key and the copied issues as the
736 # Stores the source issue id as a key and the copied issues as the
737 # value. Used to map the two togeather for issue relations.
737 # value. Used to map the two togeather for issue relations.
738 issues_map = {}
738 issues_map = {}
739
739
740 # Get issues sorted by root_id, lft so that parent issues
740 # Get issues sorted by root_id, lft so that parent issues
741 # get copied before their children
741 # get copied before their children
742 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
742 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
743 new_issue = Issue.new
743 new_issue = Issue.new
744 new_issue.copy_from(issue)
744 new_issue.copy_from(issue)
745 new_issue.project = self
745 new_issue.project = self
746 # Reassign fixed_versions by name, since names are unique per
746 # Reassign fixed_versions by name, since names are unique per
747 # project and the versions for self are not yet saved
747 # project and the versions for self are not yet saved
748 if issue.fixed_version
748 if issue.fixed_version
749 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
749 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
750 end
750 end
751 # Reassign the category by name, since names are unique per
751 # Reassign the category by name, since names are unique per
752 # project and the categories for self are not yet saved
752 # project and the categories for self are not yet saved
753 if issue.category
753 if issue.category
754 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
754 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
755 end
755 end
756 # Parent issue
756 # Parent issue
757 if issue.parent_id
757 if issue.parent_id
758 if copied_parent = issues_map[issue.parent_id]
758 if copied_parent = issues_map[issue.parent_id]
759 new_issue.parent_issue_id = copied_parent.id
759 new_issue.parent_issue_id = copied_parent.id
760 end
760 end
761 end
761 end
762
762
763 self.issues << new_issue
763 self.issues << new_issue
764 if new_issue.new_record?
764 if new_issue.new_record?
765 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
765 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
766 else
766 else
767 issues_map[issue.id] = new_issue unless new_issue.new_record?
767 issues_map[issue.id] = new_issue unless new_issue.new_record?
768 end
768 end
769 end
769 end
770
770
771 # Relations after in case issues related each other
771 # Relations after in case issues related each other
772 project.issues.each do |issue|
772 project.issues.each do |issue|
773 new_issue = issues_map[issue.id]
773 new_issue = issues_map[issue.id]
774 unless new_issue
774 unless new_issue
775 # Issue was not copied
775 # Issue was not copied
776 next
776 next
777 end
777 end
778
778
779 # Relations
779 # Relations
780 issue.relations_from.each do |source_relation|
780 issue.relations_from.each do |source_relation|
781 new_issue_relation = IssueRelation.new
781 new_issue_relation = IssueRelation.new
782 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
782 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
783 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
783 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
784 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
784 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
785 new_issue_relation.issue_to = source_relation.issue_to
785 new_issue_relation.issue_to = source_relation.issue_to
786 end
786 end
787 new_issue.relations_from << new_issue_relation
787 new_issue.relations_from << new_issue_relation
788 end
788 end
789
789
790 issue.relations_to.each do |source_relation|
790 issue.relations_to.each do |source_relation|
791 new_issue_relation = IssueRelation.new
791 new_issue_relation = IssueRelation.new
792 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
792 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
793 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
793 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
794 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
794 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
795 new_issue_relation.issue_from = source_relation.issue_from
795 new_issue_relation.issue_from = source_relation.issue_from
796 end
796 end
797 new_issue.relations_to << new_issue_relation
797 new_issue.relations_to << new_issue_relation
798 end
798 end
799 end
799 end
800 end
800 end
801
801
802 # Copies members from +project+
802 # Copies members from +project+
803 def copy_members(project)
803 def copy_members(project)
804 # Copy users first, then groups to handle members with inherited and given roles
804 # Copy users first, then groups to handle members with inherited and given roles
805 members_to_copy = []
805 members_to_copy = []
806 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
806 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
807 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
807 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
808
808
809 members_to_copy.each do |member|
809 members_to_copy.each do |member|
810 new_member = Member.new
810 new_member = Member.new
811 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
811 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
812 # only copy non inherited roles
812 # only copy non inherited roles
813 # inherited roles will be added when copying the group membership
813 # inherited roles will be added when copying the group membership
814 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
814 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
815 next if role_ids.empty?
815 next if role_ids.empty?
816 new_member.role_ids = role_ids
816 new_member.role_ids = role_ids
817 new_member.project = self
817 new_member.project = self
818 self.members << new_member
818 self.members << new_member
819 end
819 end
820 end
820 end
821
821
822 # Copies queries from +project+
822 # Copies queries from +project+
823 def copy_queries(project)
823 def copy_queries(project)
824 project.queries.each do |query|
824 project.queries.each do |query|
825 new_query = ::Query.new
825 new_query = ::Query.new
826 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
826 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
827 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
827 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
828 new_query.project = self
828 new_query.project = self
829 new_query.user_id = query.user_id
829 new_query.user_id = query.user_id
830 self.queries << new_query
830 self.queries << new_query
831 end
831 end
832 end
832 end
833
833
834 # Copies boards from +project+
834 # Copies boards from +project+
835 def copy_boards(project)
835 def copy_boards(project)
836 project.boards.each do |board|
836 project.boards.each do |board|
837 new_board = Board.new
837 new_board = Board.new
838 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
838 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
839 new_board.project = self
839 new_board.project = self
840 self.boards << new_board
840 self.boards << new_board
841 end
841 end
842 end
842 end
843
843
844 def allowed_permissions
844 def allowed_permissions
845 @allowed_permissions ||= begin
845 @allowed_permissions ||= begin
846 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
846 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
847 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
847 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
848 end
848 end
849 end
849 end
850
850
851 def allowed_actions
851 def allowed_actions
852 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
852 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
853 end
853 end
854
854
855 # Returns all the active Systemwide and project specific activities
855 # Returns all the active Systemwide and project specific activities
856 def active_activities
856 def active_activities
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
858
858
859 if overridden_activity_ids.empty?
859 if overridden_activity_ids.empty?
860 return TimeEntryActivity.shared.active
860 return TimeEntryActivity.shared.active
861 else
861 else
862 return system_activities_and_project_overrides
862 return system_activities_and_project_overrides
863 end
863 end
864 end
864 end
865
865
866 # Returns all the Systemwide and project specific activities
866 # Returns all the Systemwide and project specific activities
867 # (inactive and active)
867 # (inactive and active)
868 def all_activities
868 def all_activities
869 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
869 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
870
870
871 if overridden_activity_ids.empty?
871 if overridden_activity_ids.empty?
872 return TimeEntryActivity.shared
872 return TimeEntryActivity.shared
873 else
873 else
874 return system_activities_and_project_overrides(true)
874 return system_activities_and_project_overrides(true)
875 end
875 end
876 end
876 end
877
877
878 # Returns the systemwide active activities merged with the project specific overrides
878 # Returns the systemwide active activities merged with the project specific overrides
879 def system_activities_and_project_overrides(include_inactive=false)
879 def system_activities_and_project_overrides(include_inactive=false)
880 if include_inactive
880 if include_inactive
881 return TimeEntryActivity.shared.
881 return TimeEntryActivity.shared.
882 find(:all,
882 find(:all,
883 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
883 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
884 self.time_entry_activities
884 self.time_entry_activities
885 else
885 else
886 return TimeEntryActivity.shared.active.
886 return TimeEntryActivity.shared.active.
887 find(:all,
887 find(:all,
888 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
888 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
889 self.time_entry_activities.active
889 self.time_entry_activities.active
890 end
890 end
891 end
891 end
892
892
893 # Archives subprojects recursively
893 # Archives subprojects recursively
894 def archive!
894 def archive!
895 children.each do |subproject|
895 children.each do |subproject|
896 subproject.send :archive!
896 subproject.send :archive!
897 end
897 end
898 update_attribute :status, STATUS_ARCHIVED
898 update_attribute :status, STATUS_ARCHIVED
899 end
899 end
900 end
900 end
@@ -1,954 +1,962
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApplicationHelperTest < ActionView::TestCase
20 class ApplicationHelperTest < ActionView::TestCase
21 fixtures :projects, :roles, :enabled_modules, :users,
21 fixtures :projects, :roles, :enabled_modules, :users,
22 :repositories, :changesets,
22 :repositories, :changesets,
23 :trackers, :issue_statuses, :issues, :versions, :documents,
23 :trackers, :issue_statuses, :issues, :versions, :documents,
24 :wikis, :wiki_pages, :wiki_contents,
24 :wikis, :wiki_pages, :wiki_contents,
25 :boards, :messages, :news,
25 :boards, :messages, :news,
26 :attachments, :enumerations
26 :attachments, :enumerations
27
27
28 def setup
28 def setup
29 super
29 super
30 set_tmp_attachments_directory
30 set_tmp_attachments_directory
31 end
31 end
32
32
33 context "#link_to_if_authorized" do
33 context "#link_to_if_authorized" do
34 context "authorized user" do
34 context "authorized user" do
35 should "be tested"
35 should "be tested"
36 end
36 end
37
37
38 context "unauthorized user" do
38 context "unauthorized user" do
39 should "be tested"
39 should "be tested"
40 end
40 end
41
41
42 should "allow using the :controller and :action for the target link" do
42 should "allow using the :controller and :action for the target link" do
43 User.current = User.find_by_login('admin')
43 User.current = User.find_by_login('admin')
44
44
45 @project = Issue.first.project # Used by helper
45 @project = Issue.first.project # Used by helper
46 response = link_to_if_authorized("By controller/action",
46 response = link_to_if_authorized("By controller/action",
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 assert_match /href/, response
48 assert_match /href/, response
49 end
49 end
50
50
51 end
51 end
52
52
53 def test_auto_links
53 def test_auto_links
54 to_test = {
54 to_test = {
55 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
55 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
56 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
56 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
57 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
57 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
58 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
58 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
59 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
60 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
60 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
61 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
61 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
62 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
62 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
63 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
63 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
64 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
64 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
65 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
65 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
66 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
66 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
67 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
67 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
68 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
68 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
69 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
69 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
70 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
70 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
71 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
71 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
72 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
72 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
73 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
73 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
74 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
74 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
75 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
75 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
76 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
76 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
77 # two exclamation marks
77 # two exclamation marks
78 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
78 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
79 # escaping
79 # escaping
80 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
80 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
81 # wrap in angle brackets
81 # wrap in angle brackets
82 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
82 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
83 }
83 }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85 end
85 end
86
86
87 def test_auto_mailto
87 def test_auto_mailto
88 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
88 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
89 textilizable('test@foo.bar')
89 textilizable('test@foo.bar')
90 end
90 end
91
91
92 def test_inline_images
92 def test_inline_images
93 to_test = {
93 to_test = {
94 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
94 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
95 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
95 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
96 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
96 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
97 # inline styles should be stripped
97 # inline styles should be stripped
98 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
98 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
99 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
99 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
100 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
100 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
101 }
101 }
102 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
102 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
103 end
103 end
104
104
105 def test_inline_images_inside_tags
105 def test_inline_images_inside_tags
106 raw = <<-RAW
106 raw = <<-RAW
107 h1. !foo.png! Heading
107 h1. !foo.png! Heading
108
108
109 Centered image:
109 Centered image:
110
110
111 p=. !bar.gif!
111 p=. !bar.gif!
112 RAW
112 RAW
113
113
114 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
114 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
115 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
115 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
116 end
116 end
117
117
118 def test_attached_images
118 def test_attached_images
119 to_test = {
119 to_test = {
120 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
120 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
121 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
121 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
122 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
123 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
123 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
124 # link image
124 # link image
125 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
125 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
126 }
126 }
127 attachments = Attachment.find(:all)
127 attachments = Attachment.find(:all)
128 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
128 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
129 end
129 end
130
130
131 def test_attached_images_filename_extension
131 def test_attached_images_filename_extension
132 set_tmp_attachments_directory
132 set_tmp_attachments_directory
133 a1 = Attachment.new(
133 a1 = Attachment.new(
134 :container => Issue.find(1),
134 :container => Issue.find(1),
135 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
135 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
136 :author => User.find(1))
136 :author => User.find(1))
137 assert a1.save
137 assert a1.save
138 assert_equal "testtest.JPG", a1.filename
138 assert_equal "testtest.JPG", a1.filename
139 assert_equal "image/jpeg", a1.content_type
139 assert_equal "image/jpeg", a1.content_type
140 assert a1.image?
140 assert a1.image?
141
141
142 a2 = Attachment.new(
142 a2 = Attachment.new(
143 :container => Issue.find(1),
143 :container => Issue.find(1),
144 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
144 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
145 :author => User.find(1))
145 :author => User.find(1))
146 assert a2.save
146 assert a2.save
147 assert_equal "testtest.jpeg", a2.filename
147 assert_equal "testtest.jpeg", a2.filename
148 assert_equal "image/jpeg", a2.content_type
148 assert_equal "image/jpeg", a2.content_type
149 assert a2.image?
149 assert a2.image?
150
150
151 a3 = Attachment.new(
151 a3 = Attachment.new(
152 :container => Issue.find(1),
152 :container => Issue.find(1),
153 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
153 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
154 :author => User.find(1))
154 :author => User.find(1))
155 assert a3.save
155 assert a3.save
156 assert_equal "testtest.JPE", a3.filename
156 assert_equal "testtest.JPE", a3.filename
157 assert_equal "image/jpeg", a3.content_type
157 assert_equal "image/jpeg", a3.content_type
158 assert a3.image?
158 assert a3.image?
159
159
160 a4 = Attachment.new(
160 a4 = Attachment.new(
161 :container => Issue.find(1),
161 :container => Issue.find(1),
162 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
162 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
163 :author => User.find(1))
163 :author => User.find(1))
164 assert a4.save
164 assert a4.save
165 assert_equal "Testtest.BMP", a4.filename
165 assert_equal "Testtest.BMP", a4.filename
166 assert_equal "image/x-ms-bmp", a4.content_type
166 assert_equal "image/x-ms-bmp", a4.content_type
167 assert a4.image?
167 assert a4.image?
168
168
169 to_test = {
169 to_test = {
170 'Inline image: !testtest.jpg!' =>
170 'Inline image: !testtest.jpg!' =>
171 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
171 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
172 'Inline image: !testtest.jpeg!' =>
172 'Inline image: !testtest.jpeg!' =>
173 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
173 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
174 'Inline image: !testtest.jpe!' =>
174 'Inline image: !testtest.jpe!' =>
175 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
175 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
176 'Inline image: !testtest.bmp!' =>
176 'Inline image: !testtest.bmp!' =>
177 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
177 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
178 }
178 }
179
179
180 attachments = [a1, a2, a3, a4]
180 attachments = [a1, a2, a3, a4]
181 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
181 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
182 end
182 end
183
183
184 def test_attached_images_should_read_later
184 def test_attached_images_should_read_later
185 set_fixtures_attachments_directory
185 set_fixtures_attachments_directory
186 a1 = Attachment.find(16)
186 a1 = Attachment.find(16)
187 assert_equal "testfile.png", a1.filename
187 assert_equal "testfile.png", a1.filename
188 assert a1.readable?
188 assert a1.readable?
189 assert (! a1.visible?(User.anonymous))
189 assert (! a1.visible?(User.anonymous))
190 assert a1.visible?(User.find(2))
190 assert a1.visible?(User.find(2))
191 a2 = Attachment.find(17)
191 a2 = Attachment.find(17)
192 assert_equal "testfile.PNG", a2.filename
192 assert_equal "testfile.PNG", a2.filename
193 assert a2.readable?
193 assert a2.readable?
194 assert (! a2.visible?(User.anonymous))
194 assert (! a2.visible?(User.anonymous))
195 assert a2.visible?(User.find(2))
195 assert a2.visible?(User.find(2))
196 assert a1.created_on < a2.created_on
196 assert a1.created_on < a2.created_on
197
197
198 to_test = {
198 to_test = {
199 'Inline image: !testfile.png!' =>
199 'Inline image: !testfile.png!' =>
200 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
200 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
201 'Inline image: !Testfile.PNG!' =>
201 'Inline image: !Testfile.PNG!' =>
202 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
202 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
203 }
203 }
204 attachments = [a1, a2]
204 attachments = [a1, a2]
205 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
205 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
206 set_tmp_attachments_directory
206 set_tmp_attachments_directory
207 end
207 end
208
208
209 def test_textile_external_links
209 def test_textile_external_links
210 to_test = {
210 to_test = {
211 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
211 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
212 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
212 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
213 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
213 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
214 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
214 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
215 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
215 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
216 # no multiline link text
216 # no multiline link text
217 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
217 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
218 # mailto link
218 # mailto link
219 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
219 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
220 # two exclamation marks
220 # two exclamation marks
221 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
221 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
222 # escaping
222 # escaping
223 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
223 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
224 }
224 }
225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
226 end
226 end
227
227
228 def test_redmine_links
228 def test_redmine_links
229 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
229 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
230 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
230 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
231
231
232 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
232 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
233 :class => 'changeset', :title => 'My very first commit')
233 :class => 'changeset', :title => 'My very first commit')
234 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
234 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
235 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
235 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
236
236
237 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
237 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
238 :class => 'document')
238 :class => 'document')
239
239
240 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
240 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
241 :class => 'version')
241 :class => 'version')
242
242
243 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
243 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
244
244
245 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
245 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
246
246
247 news_url = {:controller => 'news', :action => 'show', :id => 1}
247 news_url = {:controller => 'news', :action => 'show', :id => 1}
248
248
249 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
249 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
250
250
251 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
251 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
252 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
252 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
253
253
254 to_test = {
254 to_test = {
255 # tickets
255 # tickets
256 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
256 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
257 # changesets
257 # changesets
258 'r1' => changeset_link,
258 'r1' => changeset_link,
259 'r1.' => "#{changeset_link}.",
259 'r1.' => "#{changeset_link}.",
260 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
260 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
261 'r1,r2' => "#{changeset_link},#{changeset_link2}",
261 'r1,r2' => "#{changeset_link},#{changeset_link2}",
262 # documents
262 # documents
263 'document#1' => document_link,
263 'document#1' => document_link,
264 'document:"Test document"' => document_link,
264 'document:"Test document"' => document_link,
265 # versions
265 # versions
266 'version#2' => version_link,
266 'version#2' => version_link,
267 'version:1.0' => version_link,
267 'version:1.0' => version_link,
268 'version:"1.0"' => version_link,
268 'version:"1.0"' => version_link,
269 # source
269 # source
270 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
270 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
271 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
271 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
272 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
272 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
273 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
273 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
274 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
274 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
275 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
275 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
276 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
276 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
277 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
277 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
278 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
278 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
279 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
279 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
280 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
280 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
281 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
281 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
282 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
282 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
283 # forum
283 # forum
284 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
284 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
285 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
285 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
286 # message
286 # message
287 'message#4' => link_to('Post 2', message_url, :class => 'message'),
287 'message#4' => link_to('Post 2', message_url, :class => 'message'),
288 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
288 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
289 # news
289 # news
290 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
290 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
291 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
291 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
292 # project
292 # project
293 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
293 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
294 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
294 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
295 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
295 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
296 # escaping
296 # escaping
297 '!#3.' => '#3.',
297 '!#3.' => '#3.',
298 '!r1' => 'r1',
298 '!r1' => 'r1',
299 '!document#1' => 'document#1',
299 '!document#1' => 'document#1',
300 '!document:"Test document"' => 'document:"Test document"',
300 '!document:"Test document"' => 'document:"Test document"',
301 '!version#2' => 'version#2',
301 '!version#2' => 'version#2',
302 '!version:1.0' => 'version:1.0',
302 '!version:1.0' => 'version:1.0',
303 '!version:"1.0"' => 'version:"1.0"',
303 '!version:"1.0"' => 'version:"1.0"',
304 '!source:/some/file' => 'source:/some/file',
304 '!source:/some/file' => 'source:/some/file',
305 # not found
305 # not found
306 '#0123456789' => '#0123456789',
306 '#0123456789' => '#0123456789',
307 # invalid expressions
307 # invalid expressions
308 'source:' => 'source:',
308 'source:' => 'source:',
309 # url hash
309 # url hash
310 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
310 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
311 }
311 }
312 @project = Project.find(1)
312 @project = Project.find(1)
313 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
313 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
314 end
314 end
315
315
316 def test_cross_project_redmine_links
316 def test_cross_project_redmine_links
317 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
317 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
318 :class => 'source')
318 :class => 'source')
319
319
320 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
320 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
321 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
321 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
322
322
323 to_test = {
323 to_test = {
324 # documents
324 # documents
325 'document:"Test document"' => 'document:"Test document"',
325 'document:"Test document"' => 'document:"Test document"',
326 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
326 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
327 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
327 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
328 # versions
328 # versions
329 'version:"1.0"' => 'version:"1.0"',
329 'version:"1.0"' => 'version:"1.0"',
330 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
330 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
331 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
331 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
332 # changeset
332 # changeset
333 'r2' => 'r2',
333 'r2' => 'r2',
334 'ecookbook:r2' => changeset_link,
334 'ecookbook:r2' => changeset_link,
335 'invalid:r2' => 'invalid:r2',
335 'invalid:r2' => 'invalid:r2',
336 # source
336 # source
337 'source:/some/file' => 'source:/some/file',
337 'source:/some/file' => 'source:/some/file',
338 'ecookbook:source:/some/file' => source_link,
338 'ecookbook:source:/some/file' => source_link,
339 'invalid:source:/some/file' => 'invalid:source:/some/file',
339 'invalid:source:/some/file' => 'invalid:source:/some/file',
340 }
340 }
341 @project = Project.find(3)
341 @project = Project.find(3)
342 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
342 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
343 end
343 end
344
344
345 def test_multiple_repositories_redmine_links
345 def test_multiple_repositories_redmine_links
346 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
346 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
347 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
347 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
348 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
348 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
349 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
349 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
350
350
351 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
351 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
352 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
352 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
353 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
353 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
354 :class => 'changeset', :title => '')
354 :class => 'changeset', :title => '')
355 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
355 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
356 :class => 'changeset', :title => '')
356 :class => 'changeset', :title => '')
357
357
358 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
358 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
359 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
359 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
360
360
361 to_test = {
361 to_test = {
362 'r2' => changeset_link,
362 'r2' => changeset_link,
363 'svn1|r123' => svn_changeset_link,
363 'svn1|r123' => svn_changeset_link,
364 'invalid|r123' => 'invalid|r123',
364 'invalid|r123' => 'invalid|r123',
365 'commit:hg1|abcd' => hg_changeset_link,
365 'commit:hg1|abcd' => hg_changeset_link,
366 'commit:invalid|abcd' => 'commit:invalid|abcd',
366 'commit:invalid|abcd' => 'commit:invalid|abcd',
367 # source
367 # source
368 'source:some/file' => source_link,
368 'source:some/file' => source_link,
369 'source:hg1|some/file' => hg_source_link,
369 'source:hg1|some/file' => hg_source_link,
370 'source:invalid|some/file' => 'source:invalid|some/file',
370 'source:invalid|some/file' => 'source:invalid|some/file',
371 }
371 }
372
372
373 @project = Project.find(1)
373 @project = Project.find(1)
374 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
374 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
375 end
375 end
376
376
377 def test_cross_project_multiple_repositories_redmine_links
377 def test_cross_project_multiple_repositories_redmine_links
378 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
378 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
379 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
379 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
380 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
380 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
381 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
381 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
382
382
383 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
383 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
384 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
384 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
385 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
385 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
386 :class => 'changeset', :title => '')
386 :class => 'changeset', :title => '')
387 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
387 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
388 :class => 'changeset', :title => '')
388 :class => 'changeset', :title => '')
389
389
390 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
390 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
391 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
391 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
392
392
393 to_test = {
393 to_test = {
394 'ecookbook:r2' => changeset_link,
394 'ecookbook:r2' => changeset_link,
395 'ecookbook:svn1|r123' => svn_changeset_link,
395 'ecookbook:svn1|r123' => svn_changeset_link,
396 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
396 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
397 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
397 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
398 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
398 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
399 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
399 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
400 # source
400 # source
401 'ecookbook:source:some/file' => source_link,
401 'ecookbook:source:some/file' => source_link,
402 'ecookbook:source:hg1|some/file' => hg_source_link,
402 'ecookbook:source:hg1|some/file' => hg_source_link,
403 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
403 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
404 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
404 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
405 }
405 }
406
406
407 @project = Project.find(3)
407 @project = Project.find(3)
408 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
408 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
409 end
409 end
410
410
411 def test_redmine_links_git_commit
411 def test_redmine_links_git_commit
412 changeset_link = link_to('abcd',
412 changeset_link = link_to('abcd',
413 {
413 {
414 :controller => 'repositories',
414 :controller => 'repositories',
415 :action => 'revision',
415 :action => 'revision',
416 :id => 'subproject1',
416 :id => 'subproject1',
417 :rev => 'abcd',
417 :rev => 'abcd',
418 },
418 },
419 :class => 'changeset', :title => 'test commit')
419 :class => 'changeset', :title => 'test commit')
420 to_test = {
420 to_test = {
421 'commit:abcd' => changeset_link,
421 'commit:abcd' => changeset_link,
422 }
422 }
423 @project = Project.find(3)
423 @project = Project.find(3)
424 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
424 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
425 assert r
425 assert r
426 c = Changeset.new(:repository => r,
426 c = Changeset.new(:repository => r,
427 :committed_on => Time.now,
427 :committed_on => Time.now,
428 :revision => 'abcd',
428 :revision => 'abcd',
429 :scmid => 'abcd',
429 :scmid => 'abcd',
430 :comments => 'test commit')
430 :comments => 'test commit')
431 assert( c.save )
431 assert( c.save )
432 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
432 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
433 end
433 end
434
434
435 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
435 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
436 def test_redmine_links_darcs_commit
436 def test_redmine_links_darcs_commit
437 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
437 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
438 {
438 {
439 :controller => 'repositories',
439 :controller => 'repositories',
440 :action => 'revision',
440 :action => 'revision',
441 :id => 'subproject1',
441 :id => 'subproject1',
442 :rev => '123',
442 :rev => '123',
443 },
443 },
444 :class => 'changeset', :title => 'test commit')
444 :class => 'changeset', :title => 'test commit')
445 to_test = {
445 to_test = {
446 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
446 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
447 }
447 }
448 @project = Project.find(3)
448 @project = Project.find(3)
449 r = Repository::Darcs.create!(
449 r = Repository::Darcs.create!(
450 :project => @project, :url => '/tmp/test/darcs',
450 :project => @project, :url => '/tmp/test/darcs',
451 :log_encoding => 'UTF-8')
451 :log_encoding => 'UTF-8')
452 assert r
452 assert r
453 c = Changeset.new(:repository => r,
453 c = Changeset.new(:repository => r,
454 :committed_on => Time.now,
454 :committed_on => Time.now,
455 :revision => '123',
455 :revision => '123',
456 :scmid => '20080308225258-98289-abcd456efg.gz',
456 :scmid => '20080308225258-98289-abcd456efg.gz',
457 :comments => 'test commit')
457 :comments => 'test commit')
458 assert( c.save )
458 assert( c.save )
459 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
459 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
460 end
460 end
461
461
462 def test_redmine_links_mercurial_commit
462 def test_redmine_links_mercurial_commit
463 changeset_link_rev = link_to('r123',
463 changeset_link_rev = link_to('r123',
464 {
464 {
465 :controller => 'repositories',
465 :controller => 'repositories',
466 :action => 'revision',
466 :action => 'revision',
467 :id => 'subproject1',
467 :id => 'subproject1',
468 :rev => '123' ,
468 :rev => '123' ,
469 },
469 },
470 :class => 'changeset', :title => 'test commit')
470 :class => 'changeset', :title => 'test commit')
471 changeset_link_commit = link_to('abcd',
471 changeset_link_commit = link_to('abcd',
472 {
472 {
473 :controller => 'repositories',
473 :controller => 'repositories',
474 :action => 'revision',
474 :action => 'revision',
475 :id => 'subproject1',
475 :id => 'subproject1',
476 :rev => 'abcd' ,
476 :rev => 'abcd' ,
477 },
477 },
478 :class => 'changeset', :title => 'test commit')
478 :class => 'changeset', :title => 'test commit')
479 to_test = {
479 to_test = {
480 'r123' => changeset_link_rev,
480 'r123' => changeset_link_rev,
481 'commit:abcd' => changeset_link_commit,
481 'commit:abcd' => changeset_link_commit,
482 }
482 }
483 @project = Project.find(3)
483 @project = Project.find(3)
484 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
484 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
485 assert r
485 assert r
486 c = Changeset.new(:repository => r,
486 c = Changeset.new(:repository => r,
487 :committed_on => Time.now,
487 :committed_on => Time.now,
488 :revision => '123',
488 :revision => '123',
489 :scmid => 'abcd',
489 :scmid => 'abcd',
490 :comments => 'test commit')
490 :comments => 'test commit')
491 assert( c.save )
491 assert( c.save )
492 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
492 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
493 end
493 end
494
494
495 def test_attachment_links
495 def test_attachment_links
496 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
496 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
497 to_test = {
497 to_test = {
498 'attachment:error281.txt' => attachment_link
498 'attachment:error281.txt' => attachment_link
499 }
499 }
500 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
500 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
501 end
501 end
502
502
503 def test_wiki_links
503 def test_wiki_links
504 to_test = {
504 to_test = {
505 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
505 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
506 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
506 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
507 # title content should be formatted
507 # title content should be formatted
508 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
508 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
509 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
509 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
510 # link with anchor
510 # link with anchor
511 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
511 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
512 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
512 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
513 # page that doesn't exist
513 # page that doesn't exist
514 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
514 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
515 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
515 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
516 # link to another project wiki
516 # link to another project wiki
517 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
517 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
518 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
518 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
519 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
519 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
520 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
520 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
521 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
521 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
522 # striked through link
522 # striked through link
523 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
523 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
524 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
524 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
525 # escaping
525 # escaping
526 '![[Another page|Page]]' => '[[Another page|Page]]',
526 '![[Another page|Page]]' => '[[Another page|Page]]',
527 # project does not exist
527 # project does not exist
528 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
528 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
529 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
529 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
530 }
530 }
531
531
532 @project = Project.find(1)
532 @project = Project.find(1)
533 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
533 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
534 end
534 end
535
535
536 def test_wiki_links_within_local_file_generation_context
536 def test_wiki_links_within_local_file_generation_context
537
537
538 to_test = {
538 to_test = {
539 # link to a page
539 # link to a page
540 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
540 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
541 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
541 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
542 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
542 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
543 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
543 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
544 # page that doesn't exist
544 # page that doesn't exist
545 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
545 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
546 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
546 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
547 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
547 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
548 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
548 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
549 }
549 }
550
550
551 @project = Project.find(1)
551 @project = Project.find(1)
552
552
553 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
553 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
554 end
554 end
555
555
556 def test_wiki_links_within_wiki_page_context
556 def test_wiki_links_within_wiki_page_context
557
557
558 page = WikiPage.find_by_title('Another_page' )
558 page = WikiPage.find_by_title('Another_page' )
559
559
560 to_test = {
560 to_test = {
561 # link to another page
561 # link to another page
562 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
562 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
563 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
563 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
564 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
564 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
565 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
565 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
566 # link to the current page
566 # link to the current page
567 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
567 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
568 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
568 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
569 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
569 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
570 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
570 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
571 # page that doesn't exist
571 # page that doesn't exist
572 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
572 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
573 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
573 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
574 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
574 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
575 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
575 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
576 }
576 }
577
577
578 @project = Project.find(1)
578 @project = Project.find(1)
579
579
580 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
580 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
581 end
581 end
582
582
583 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
583 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
584
584
585 to_test = {
585 to_test = {
586 # link to a page
586 # link to a page
587 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
587 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
588 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
588 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
589 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
589 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
590 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
590 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
591 # page that doesn't exist
591 # page that doesn't exist
592 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
592 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
593 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
593 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
594 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
594 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
595 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
595 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
596 }
596 }
597
597
598 @project = Project.find(1)
598 @project = Project.find(1)
599
599
600 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
600 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
601 end
601 end
602
602
603 def test_html_tags
603 def test_html_tags
604 to_test = {
604 to_test = {
605 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
605 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
606 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
606 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
607 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
607 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
608 # do not escape pre/code tags
608 # do not escape pre/code tags
609 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
609 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
610 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
610 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
611 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
611 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
612 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
612 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
613 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
613 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
614 # remove attributes except class
614 # remove attributes except class
615 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
615 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
616 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
616 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
617 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
617 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
618 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
618 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
619 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
619 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
620 # xss
620 # xss
621 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
621 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
622 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
622 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
623 }
623 }
624 to_test.each { |text, result| assert_equal result, textilizable(text) }
624 to_test.each { |text, result| assert_equal result, textilizable(text) }
625 end
625 end
626
626
627 def test_allowed_html_tags
627 def test_allowed_html_tags
628 to_test = {
628 to_test = {
629 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
629 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
630 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
630 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
631 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
631 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
632 }
632 }
633 to_test.each { |text, result| assert_equal result, textilizable(text) }
633 to_test.each { |text, result| assert_equal result, textilizable(text) }
634 end
634 end
635
635
636 def test_pre_tags
636 def test_pre_tags
637 raw = <<-RAW
637 raw = <<-RAW
638 Before
638 Before
639
639
640 <pre>
640 <pre>
641 <prepared-statement-cache-size>32</prepared-statement-cache-size>
641 <prepared-statement-cache-size>32</prepared-statement-cache-size>
642 </pre>
642 </pre>
643
643
644 After
644 After
645 RAW
645 RAW
646
646
647 expected = <<-EXPECTED
647 expected = <<-EXPECTED
648 <p>Before</p>
648 <p>Before</p>
649 <pre>
649 <pre>
650 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
650 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
651 </pre>
651 </pre>
652 <p>After</p>
652 <p>After</p>
653 EXPECTED
653 EXPECTED
654
654
655 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
655 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
656 end
656 end
657
657
658 def test_pre_content_should_not_parse_wiki_and_redmine_links
658 def test_pre_content_should_not_parse_wiki_and_redmine_links
659 raw = <<-RAW
659 raw = <<-RAW
660 [[CookBook documentation]]
660 [[CookBook documentation]]
661
661
662 #1
662 #1
663
663
664 <pre>
664 <pre>
665 [[CookBook documentation]]
665 [[CookBook documentation]]
666
666
667 #1
667 #1
668 </pre>
668 </pre>
669 RAW
669 RAW
670
670
671 expected = <<-EXPECTED
671 expected = <<-EXPECTED
672 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
672 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
673 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
673 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
674 <pre>
674 <pre>
675 [[CookBook documentation]]
675 [[CookBook documentation]]
676
676
677 #1
677 #1
678 </pre>
678 </pre>
679 EXPECTED
679 EXPECTED
680
680
681 @project = Project.find(1)
681 @project = Project.find(1)
682 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
682 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
683 end
683 end
684
684
685 def test_non_closing_pre_blocks_should_be_closed
685 def test_non_closing_pre_blocks_should_be_closed
686 raw = <<-RAW
686 raw = <<-RAW
687 <pre><code>
687 <pre><code>
688 RAW
688 RAW
689
689
690 expected = <<-EXPECTED
690 expected = <<-EXPECTED
691 <pre><code>
691 <pre><code>
692 </code></pre>
692 </code></pre>
693 EXPECTED
693 EXPECTED
694
694
695 @project = Project.find(1)
695 @project = Project.find(1)
696 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
696 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
697 end
697 end
698
698
699 def test_syntax_highlight
699 def test_syntax_highlight
700 raw = <<-RAW
700 raw = <<-RAW
701 <pre><code class="ruby">
701 <pre><code class="ruby">
702 # Some ruby code here
702 # Some ruby code here
703 </code></pre>
703 </code></pre>
704 RAW
704 RAW
705
705
706 expected = <<-EXPECTED
706 expected = <<-EXPECTED
707 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
707 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
708 </code></pre>
708 </code></pre>
709 EXPECTED
709 EXPECTED
710
710
711 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
711 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
712 end
712 end
713
713
714 def test_wiki_links_in_tables
714 def test_wiki_links_in_tables
715 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
715 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
716 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
716 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
717 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
717 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
718 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
718 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
719 }
719 }
720 @project = Project.find(1)
720 @project = Project.find(1)
721 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
721 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
722 end
722 end
723
723
724 def test_text_formatting
724 def test_text_formatting
725 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
725 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
726 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
726 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
727 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
727 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
728 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
728 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
729 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
729 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
730 }
730 }
731 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
731 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
732 end
732 end
733
733
734 def test_wiki_horizontal_rule
734 def test_wiki_horizontal_rule
735 assert_equal '<hr />', textilizable('---')
735 assert_equal '<hr />', textilizable('---')
736 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
736 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
737 end
737 end
738
738
739 def test_footnotes
739 def test_footnotes
740 raw = <<-RAW
740 raw = <<-RAW
741 This is some text[1].
741 This is some text[1].
742
742
743 fn1. This is the foot note
743 fn1. This is the foot note
744 RAW
744 RAW
745
745
746 expected = <<-EXPECTED
746 expected = <<-EXPECTED
747 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
747 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
748 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
748 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
749 EXPECTED
749 EXPECTED
750
750
751 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
751 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
752 end
752 end
753
753
754 def test_headings
754 def test_headings
755 raw = 'h1. Some heading'
755 raw = 'h1. Some heading'
756 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
756 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
757
757
758 assert_equal expected, textilizable(raw)
758 assert_equal expected, textilizable(raw)
759 end
759 end
760
760
761 def test_headings_with_special_chars
761 def test_headings_with_special_chars
762 # This test makes sure that the generated anchor names match the expected
762 # This test makes sure that the generated anchor names match the expected
763 # ones even if the heading text contains unconventional characters
763 # ones even if the heading text contains unconventional characters
764 raw = 'h1. Some heading related to version 0.5'
764 raw = 'h1. Some heading related to version 0.5'
765 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
765 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
766 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
766 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
767
767
768 assert_equal expected, textilizable(raw)
768 assert_equal expected, textilizable(raw)
769 end
769 end
770
770
771 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
771 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
772 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
772 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
773 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
773 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
774
774
775 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
775 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
776
776
777 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
777 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
778 end
778 end
779
779
780 def test_table_of_content
780 def test_table_of_content
781 raw = <<-RAW
781 raw = <<-RAW
782 {{toc}}
782 {{toc}}
783
783
784 h1. Title
784 h1. Title
785
785
786 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
786 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
787
787
788 h2. Subtitle with a [[Wiki]] link
788 h2. Subtitle with a [[Wiki]] link
789
789
790 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
790 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
791
791
792 h2. Subtitle with [[Wiki|another Wiki]] link
792 h2. Subtitle with [[Wiki|another Wiki]] link
793
793
794 h2. Subtitle with %{color:red}red text%
794 h2. Subtitle with %{color:red}red text%
795
795
796 <pre>
796 <pre>
797 some code
797 some code
798 </pre>
798 </pre>
799
799
800 h3. Subtitle with *some* _modifiers_
800 h3. Subtitle with *some* _modifiers_
801
801
802 h1. Another title
802 h1. Another title
803
803
804 h3. An "Internet link":http://www.redmine.org/ inside subtitle
804 h3. An "Internet link":http://www.redmine.org/ inside subtitle
805
805
806 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
806 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
807
807
808 RAW
808 RAW
809
809
810 expected = '<ul class="toc">' +
810 expected = '<ul class="toc">' +
811 '<li><a href="#Title">Title</a>' +
811 '<li><a href="#Title">Title</a>' +
812 '<ul>' +
812 '<ul>' +
813 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
813 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
814 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
814 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
815 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
815 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
816 '<ul>' +
816 '<ul>' +
817 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
817 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
818 '</ul>' +
818 '</ul>' +
819 '</li>' +
819 '</li>' +
820 '</ul>' +
820 '</ul>' +
821 '</li>' +
821 '</li>' +
822 '<li><a href="#Another-title">Another title</a>' +
822 '<li><a href="#Another-title">Another title</a>' +
823 '<ul>' +
823 '<ul>' +
824 '<li>' +
824 '<li>' +
825 '<ul>' +
825 '<ul>' +
826 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
826 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
827 '</ul>' +
827 '</ul>' +
828 '</li>' +
828 '</li>' +
829 '<li><a href="#Project-Name">Project Name</a></li>' +
829 '<li><a href="#Project-Name">Project Name</a></li>' +
830 '</ul>' +
830 '</ul>' +
831 '</li>' +
831 '</li>' +
832 '</ul>'
832 '</ul>'
833
833
834 @project = Project.find(1)
834 @project = Project.find(1)
835 assert textilizable(raw).gsub("\n", "").include?(expected)
835 assert textilizable(raw).gsub("\n", "").include?(expected)
836 end
836 end
837
837
838 def test_table_of_content_should_contain_included_page_headings
838 def test_table_of_content_should_contain_included_page_headings
839 raw = <<-RAW
839 raw = <<-RAW
840 {{toc}}
840 {{toc}}
841
841
842 h1. Included
842 h1. Included
843
843
844 {{include(Child_1)}}
844 {{include(Child_1)}}
845 RAW
845 RAW
846
846
847 expected = '<ul class="toc">' +
847 expected = '<ul class="toc">' +
848 '<li><a href="#Included">Included</a></li>' +
848 '<li><a href="#Included">Included</a></li>' +
849 '<li><a href="#Child-page-1">Child page 1</a></li>' +
849 '<li><a href="#Child-page-1">Child page 1</a></li>' +
850 '</ul>'
850 '</ul>'
851
851
852 @project = Project.find(1)
852 @project = Project.find(1)
853 assert textilizable(raw).gsub("\n", "").include?(expected)
853 assert textilizable(raw).gsub("\n", "").include?(expected)
854 end
854 end
855
855
856 def test_default_formatter
856 def test_default_formatter
857 with_settings :text_formatting => 'unknown' do
857 with_settings :text_formatting => 'unknown' do
858 text = 'a *link*: http://www.example.net/'
858 text = 'a *link*: http://www.example.net/'
859 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
859 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
860 end
860 end
861 end
861 end
862
862
863 def test_due_date_distance_in_words
863 def test_due_date_distance_in_words
864 to_test = { Date.today => 'Due in 0 days',
864 to_test = { Date.today => 'Due in 0 days',
865 Date.today + 1 => 'Due in 1 day',
865 Date.today + 1 => 'Due in 1 day',
866 Date.today + 100 => 'Due in about 3 months',
866 Date.today + 100 => 'Due in about 3 months',
867 Date.today + 20000 => 'Due in over 54 years',
867 Date.today + 20000 => 'Due in over 54 years',
868 Date.today - 1 => '1 day late',
868 Date.today - 1 => '1 day late',
869 Date.today - 100 => 'about 3 months late',
869 Date.today - 100 => 'about 3 months late',
870 Date.today - 20000 => 'over 54 years late',
870 Date.today - 20000 => 'over 54 years late',
871 }
871 }
872 ::I18n.locale = :en
872 ::I18n.locale = :en
873 to_test.each do |date, expected|
873 to_test.each do |date, expected|
874 assert_equal expected, due_date_distance_in_words(date)
874 assert_equal expected, due_date_distance_in_words(date)
875 end
875 end
876 end
876 end
877
877
878 def test_avatar
878 def test_avatar
879 # turn on avatars
879 # turn on avatars
880 Setting.gravatar_enabled = '1'
880 Setting.gravatar_enabled = '1'
881 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
881 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
882 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
882 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
883 assert_nil avatar('jsmith')
883 assert_nil avatar('jsmith')
884 assert_nil avatar(nil)
884 assert_nil avatar(nil)
885
885
886 # turn off avatars
886 # turn off avatars
887 Setting.gravatar_enabled = '0'
887 Setting.gravatar_enabled = '0'
888 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
888 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
889 end
889 end
890
890
891 def test_link_to_user
891 def test_link_to_user
892 user = User.find(2)
892 user = User.find(2)
893 t = link_to_user(user)
893 t = link_to_user(user)
894 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
894 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
895 end
895 end
896
896
897 def test_link_to_user_should_not_link_to_locked_user
897 def test_link_to_user_should_not_link_to_locked_user
898 user = User.find(5)
898 user = User.find(5)
899 assert user.locked?
899 assert user.locked?
900 t = link_to_user(user)
900 t = link_to_user(user)
901 assert_equal user.name, t
901 assert_equal user.name, t
902 end
902 end
903
903
904 def test_link_to_user_should_not_link_to_anonymous
904 def test_link_to_user_should_not_link_to_anonymous
905 user = User.anonymous
905 user = User.anonymous
906 assert user.anonymous?
906 assert user.anonymous?
907 t = link_to_user(user)
907 t = link_to_user(user)
908 assert_equal ::I18n.t(:label_user_anonymous), t
908 assert_equal ::I18n.t(:label_user_anonymous), t
909 end
909 end
910
910
911 def test_link_to_project
911 def test_link_to_project
912 project = Project.find(1)
912 project = Project.find(1)
913 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
913 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
914 link_to_project(project)
914 link_to_project(project)
915 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
915 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
916 link_to_project(project, :action => 'settings')
916 link_to_project(project, :action => 'settings')
917 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
917 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
918 link_to_project(project, {:only_path => false, :jump => 'blah'})
918 link_to_project(project, {:only_path => false, :jump => 'blah'})
919 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
919 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
920 link_to_project(project, {:action => 'settings'}, :class => "project")
920 link_to_project(project, {:action => 'settings'}, :class => "project")
921 end
921 end
922
922
923 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
924 # numeric identifier are no longer allowed
925 Project.update_all "identifier=25", "id=1"
926
927 assert_equal '<a href="/projects/1">eCookbook</a>',
928 link_to_project(Project.find(1))
929 end
930
923 def test_principals_options_for_select_with_users
931 def test_principals_options_for_select_with_users
924 User.current = nil
932 User.current = nil
925 users = [User.find(2), User.find(4)]
933 users = [User.find(2), User.find(4)]
926 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
934 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
927 principals_options_for_select(users)
935 principals_options_for_select(users)
928 end
936 end
929
937
930 def test_principals_options_for_select_with_selected
938 def test_principals_options_for_select_with_selected
931 User.current = nil
939 User.current = nil
932 users = [User.find(2), User.find(4)]
940 users = [User.find(2), User.find(4)]
933 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
941 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
934 principals_options_for_select(users, User.find(4))
942 principals_options_for_select(users, User.find(4))
935 end
943 end
936
944
937 def test_principals_options_for_select_with_users_and_groups
945 def test_principals_options_for_select_with_users_and_groups
938 User.current = nil
946 User.current = nil
939 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
947 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
940 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
948 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
941 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
949 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
942 principals_options_for_select(users)
950 principals_options_for_select(users)
943 end
951 end
944
952
945 def test_principals_options_for_select_with_empty_collection
953 def test_principals_options_for_select_with_empty_collection
946 assert_equal '', principals_options_for_select([])
954 assert_equal '', principals_options_for_select([])
947 end
955 end
948
956
949 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
957 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
950 users = [User.find(2), User.find(4)]
958 users = [User.find(2), User.find(4)]
951 User.current = User.find(4)
959 User.current = User.find(4)
952 assert_include '<option value="4"><< me >></option>', principals_options_for_select(users)
960 assert_include '<option value="4"><< me >></option>', principals_options_for_select(users)
953 end
961 end
954 end
962 end
General Comments 0
You need to be logged in to leave comments. Login now