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