##// END OF EJS Templates
Ability to delete multiple attachments while updating an issue (#13072)....
Jean-Philippe Lang -
r15268:19492226f7b2
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1708 +1,1723
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 attr_accessor :deleted_attachment_ids
62 63 attr_reader :current_journal
63 64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 65
65 66 validates_presence_of :subject, :project, :tracker
66 67 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 68 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 69 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 70
70 71 validates_length_of :subject, :maximum => 255
71 72 validates_inclusion_of :done_ratio, :in => 0..100
72 73 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 74 validates :start_date, :date => true
74 75 validates :due_date, :date => true
75 76 validate :validate_issue, :validate_required_fields
76 77 attr_protected :id
77 78
78 79 scope :visible, lambda {|*args|
79 80 joins(:project).
80 81 where(Issue.visible_condition(args.shift || User.current, *args))
81 82 }
82 83
83 84 scope :open, lambda {|*args|
84 85 is_closed = args.size > 0 ? !args.first : false
85 86 joins(:status).
86 87 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 88 }
88 89
89 90 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 91 scope :on_active_project, lambda {
91 92 joins(:project).
92 93 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 94 }
94 95 scope :fixed_version, lambda {|versions|
95 96 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 97 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 98 }
98 99 scope :assigned_to, lambda {|arg|
99 100 arg = Array(arg).uniq
100 101 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
101 102 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
102 103 ids.compact!
103 104 ids.any? ? where(:assigned_to_id => ids) : none
104 105 }
105 106
106 107 before_validation :clear_disabled_fields
107 108 before_create :default_assign
108 109 before_save :close_duplicates, :update_done_ratio_from_issue_status,
109 110 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
110 111 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
111 112 after_save :reschedule_following_issues, :update_nested_set_attributes,
112 :update_parent_attributes, :create_journal
113 :update_parent_attributes, :delete_selected_attachments, :create_journal
113 114 # Should be after_create but would be called before previous after_save callbacks
114 115 after_save :after_create_from_copy
115 116 after_destroy :update_parent_attributes
116 117 after_create :send_notification
117 118 # Keep it at the end of after_save callbacks
118 119 after_save :clear_assigned_to_was
119 120
120 121 # Returns a SQL conditions string used to find all issues visible by the specified user
121 122 def self.visible_condition(user, options={})
122 123 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
123 124 sql = if user.id && user.logged?
124 125 case role.issues_visibility
125 126 when 'all'
126 127 '1=1'
127 128 when 'default'
128 129 user_ids = [user.id] + user.groups.map(&:id).compact
129 130 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
130 131 when 'own'
131 132 user_ids = [user.id] + user.groups.map(&:id).compact
132 133 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
133 134 else
134 135 '1=0'
135 136 end
136 137 else
137 138 "(#{table_name}.is_private = #{connection.quoted_false})"
138 139 end
139 140 unless role.permissions_all_trackers?(:view_issues)
140 141 tracker_ids = role.permissions_tracker_ids(:view_issues)
141 142 if tracker_ids.any?
142 143 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
143 144 else
144 145 sql = '1=0'
145 146 end
146 147 end
147 148 sql
148 149 end
149 150 end
150 151
151 152 # Returns true if usr or current user is allowed to view the issue
152 153 def visible?(usr=nil)
153 154 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
154 155 visible = if user.logged?
155 156 case role.issues_visibility
156 157 when 'all'
157 158 true
158 159 when 'default'
159 160 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
160 161 when 'own'
161 162 self.author == user || user.is_or_belongs_to?(assigned_to)
162 163 else
163 164 false
164 165 end
165 166 else
166 167 !self.is_private?
167 168 end
168 169 unless role.permissions_all_trackers?(:view_issues)
169 170 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
170 171 end
171 172 visible
172 173 end
173 174 end
174 175
175 176 # Returns true if user or current user is allowed to edit or add notes to the issue
176 177 def editable?(user=User.current)
177 178 attributes_editable?(user) || notes_addable?(user)
178 179 end
179 180
180 181 # Returns true if user or current user is allowed to edit the issue
181 182 def attributes_editable?(user=User.current)
182 183 user_tracker_permission?(user, :edit_issues)
183 184 end
184 185
185 186 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
186 187 def attachments_editable?(user=User.current)
187 188 attributes_editable?(user)
188 189 end
189 190
190 191 # Returns true if user or current user is allowed to add notes to the issue
191 192 def notes_addable?(user=User.current)
192 193 user_tracker_permission?(user, :add_issue_notes)
193 194 end
194 195
195 196 # Returns true if user or current user is allowed to delete the issue
196 197 def deletable?(user=User.current)
197 198 user_tracker_permission?(user, :delete_issues)
198 199 end
199 200
200 201 def initialize(attributes=nil, *args)
201 202 super
202 203 if new_record?
203 204 # set default values for new records only
204 205 self.priority ||= IssuePriority.default
205 206 self.watcher_user_ids = []
206 207 end
207 208 end
208 209
209 210 def create_or_update
210 211 super
211 212 ensure
212 213 @status_was = nil
213 214 end
214 215 private :create_or_update
215 216
216 217 # AR#Persistence#destroy would raise and RecordNotFound exception
217 218 # if the issue was already deleted or updated (non matching lock_version).
218 219 # This is a problem when bulk deleting issues or deleting a project
219 220 # (because an issue may already be deleted if its parent was deleted
220 221 # first).
221 222 # The issue is reloaded by the nested_set before being deleted so
222 223 # the lock_version condition should not be an issue but we handle it.
223 224 def destroy
224 225 super
225 226 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
226 227 # Stale or already deleted
227 228 begin
228 229 reload
229 230 rescue ActiveRecord::RecordNotFound
230 231 # The issue was actually already deleted
231 232 @destroyed = true
232 233 return freeze
233 234 end
234 235 # The issue was stale, retry to destroy
235 236 super
236 237 end
237 238
238 239 alias :base_reload :reload
239 240 def reload(*args)
240 241 @workflow_rule_by_attribute = nil
241 242 @assignable_versions = nil
242 243 @relations = nil
243 244 @spent_hours = nil
244 245 @total_spent_hours = nil
245 246 @total_estimated_hours = nil
246 247 base_reload(*args)
247 248 end
248 249
249 250 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
250 251 def available_custom_fields
251 252 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
252 253 end
253 254
254 255 def visible_custom_field_values(user=nil)
255 256 user_real = user || User.current
256 257 custom_field_values.select do |value|
257 258 value.custom_field.visible_by?(project, user_real)
258 259 end
259 260 end
260 261
261 262 # Copies attributes from another issue, arg can be an id or an Issue
262 263 def copy_from(arg, options={})
263 264 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
264 265 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
265 266 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
266 267 self.status = issue.status
267 268 self.author = User.current
268 269 unless options[:attachments] == false
269 270 self.attachments = issue.attachments.map do |attachement|
270 271 attachement.copy(:container => self)
271 272 end
272 273 end
273 274 @copied_from = issue
274 275 @copy_options = options
275 276 self
276 277 end
277 278
278 279 # Returns an unsaved copy of the issue
279 280 def copy(attributes=nil, copy_options={})
280 281 copy = self.class.new.copy_from(self, copy_options)
281 282 copy.attributes = attributes if attributes
282 283 copy
283 284 end
284 285
285 286 # Returns true if the issue is a copy
286 287 def copy?
287 288 @copied_from.present?
288 289 end
289 290
290 291 def status_id=(status_id)
291 292 if status_id.to_s != self.status_id.to_s
292 293 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
293 294 end
294 295 self.status_id
295 296 end
296 297
297 298 # Sets the status.
298 299 def status=(status)
299 300 if status != self.status
300 301 @workflow_rule_by_attribute = nil
301 302 end
302 303 association(:status).writer(status)
303 304 end
304 305
305 306 def priority_id=(pid)
306 307 self.priority = nil
307 308 write_attribute(:priority_id, pid)
308 309 end
309 310
310 311 def category_id=(cid)
311 312 self.category = nil
312 313 write_attribute(:category_id, cid)
313 314 end
314 315
315 316 def fixed_version_id=(vid)
316 317 self.fixed_version = nil
317 318 write_attribute(:fixed_version_id, vid)
318 319 end
319 320
320 321 def tracker_id=(tracker_id)
321 322 if tracker_id.to_s != self.tracker_id.to_s
322 323 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
323 324 end
324 325 self.tracker_id
325 326 end
326 327
327 328 # Sets the tracker.
328 329 # This will set the status to the default status of the new tracker if:
329 330 # * the status was the default for the previous tracker
330 331 # * or if the status was not part of the new tracker statuses
331 332 # * or the status was nil
332 333 def tracker=(tracker)
333 334 tracker_was = self.tracker
334 335 association(:tracker).writer(tracker)
335 336 if tracker != tracker_was
336 337 if status == tracker_was.try(:default_status)
337 338 self.status = nil
338 339 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
339 340 self.status = nil
340 341 end
341 342 reassign_custom_field_values
342 343 @workflow_rule_by_attribute = nil
343 344 end
344 345 self.status ||= default_status
345 346 self.tracker
346 347 end
347 348
348 349 def project_id=(project_id)
349 350 if project_id.to_s != self.project_id.to_s
350 351 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
351 352 end
352 353 self.project_id
353 354 end
354 355
355 356 # Sets the project.
356 357 # Unless keep_tracker argument is set to true, this will change the tracker
357 358 # to the first tracker of the new project if the previous tracker is not part
358 359 # of the new project trackers.
359 360 # This will:
360 361 # * clear the fixed_version is it's no longer valid for the new project.
361 362 # * clear the parent issue if it's no longer valid for the new project.
362 363 # * set the category to the category with the same name in the new
363 364 # project if it exists, or clear it if it doesn't.
364 365 # * for new issue, set the fixed_version to the project default version
365 366 # if it's a valid fixed_version.
366 367 def project=(project, keep_tracker=false)
367 368 project_was = self.project
368 369 association(:project).writer(project)
369 370 if project_was && project && project_was != project
370 371 @assignable_versions = nil
371 372
372 373 unless keep_tracker || project.trackers.include?(tracker)
373 374 self.tracker = project.trackers.first
374 375 end
375 376 # Reassign to the category with same name if any
376 377 if category
377 378 self.category = project.issue_categories.find_by_name(category.name)
378 379 end
379 380 # Keep the fixed_version if it's still valid in the new_project
380 381 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
381 382 self.fixed_version = nil
382 383 end
383 384 # Clear the parent task if it's no longer valid
384 385 unless valid_parent_project?
385 386 self.parent_issue_id = nil
386 387 end
387 388 reassign_custom_field_values
388 389 @workflow_rule_by_attribute = nil
389 390 end
390 391 # Set fixed_version to the project default version if it's valid
391 392 if new_record? && fixed_version.nil? && project && project.default_version_id?
392 393 if project.shared_versions.open.exists?(project.default_version_id)
393 394 self.fixed_version_id = project.default_version_id
394 395 end
395 396 end
396 397 self.project
397 398 end
398 399
399 400 def description=(arg)
400 401 if arg.is_a?(String)
401 402 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
402 403 end
403 404 write_attribute(:description, arg)
404 405 end
405 406
407 def deleted_attachment_ids
408 Array(@deleted_attachment_ids).map(&:to_i)
409 end
410
406 411 # Overrides assign_attributes so that project and tracker get assigned first
407 412 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
408 413 return if new_attributes.nil?
409 414 attrs = new_attributes.dup
410 415 attrs.stringify_keys!
411 416
412 417 %w(project project_id tracker tracker_id).each do |attr|
413 418 if attrs.has_key?(attr)
414 419 send "#{attr}=", attrs.delete(attr)
415 420 end
416 421 end
417 422 send :assign_attributes_without_project_and_tracker_first, attrs, *args
418 423 end
419 424 # Do not redefine alias chain on reload (see #4838)
420 425 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
421 426
422 427 def attributes=(new_attributes)
423 428 assign_attributes new_attributes
424 429 end
425 430
426 431 def estimated_hours=(h)
427 432 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
428 433 end
429 434
430 435 safe_attributes 'project_id',
431 436 'tracker_id',
432 437 'status_id',
433 438 'category_id',
434 439 'assigned_to_id',
435 440 'priority_id',
436 441 'fixed_version_id',
437 442 'subject',
438 443 'description',
439 444 'start_date',
440 445 'due_date',
441 446 'done_ratio',
442 447 'estimated_hours',
443 448 'custom_field_values',
444 449 'custom_fields',
445 450 'lock_version',
446 451 'notes',
447 452 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
448 453
449 454 safe_attributes 'notes',
450 455 :if => lambda {|issue, user| issue.notes_addable?(user)}
451 456
452 457 safe_attributes 'private_notes',
453 458 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
454 459
455 460 safe_attributes 'watcher_user_ids',
456 461 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
457 462
458 463 safe_attributes 'is_private',
459 464 :if => lambda {|issue, user|
460 465 user.allowed_to?(:set_issues_private, issue.project) ||
461 466 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
462 467 }
463 468
464 469 safe_attributes 'parent_issue_id',
465 470 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
466 471 user.allowed_to?(:manage_subtasks, issue.project)}
467 472
473 safe_attributes 'deleted_attachment_ids',
474 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
475
468 476 def safe_attribute_names(user=nil)
469 477 names = super
470 478 names -= disabled_core_fields
471 479 names -= read_only_attribute_names(user)
472 480 if new_record?
473 481 # Make sure that project_id can always be set for new issues
474 482 names |= %w(project_id)
475 483 end
476 484 if dates_derived?
477 485 names -= %w(start_date due_date)
478 486 end
479 487 if priority_derived?
480 488 names -= %w(priority_id)
481 489 end
482 490 if done_ratio_derived?
483 491 names -= %w(done_ratio)
484 492 end
485 493 names
486 494 end
487 495
488 496 # Safely sets attributes
489 497 # Should be called from controllers instead of #attributes=
490 498 # attr_accessible is too rough because we still want things like
491 499 # Issue.new(:project => foo) to work
492 500 def safe_attributes=(attrs, user=User.current)
493 501 return unless attrs.is_a?(Hash)
494 502
495 503 attrs = attrs.deep_dup
496 504
497 505 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
498 506 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
499 507 if allowed_target_projects(user).where(:id => p.to_i).exists?
500 508 self.project_id = p
501 509 end
502 510
503 511 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
504 512 # Discard submitted category on previous project
505 513 attrs.delete('category_id')
506 514 end
507 515 end
508 516
509 517 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
510 518 if allowed_target_trackers(user).where(:id => t.to_i).exists?
511 519 self.tracker_id = t
512 520 end
513 521 end
514 522 if project
515 523 # Set a default tracker to accept custom field values
516 524 # even if tracker is not specified
517 525 self.tracker ||= allowed_target_trackers(user).first
518 526 end
519 527
520 528 statuses_allowed = new_statuses_allowed_to(user)
521 529 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
522 530 if statuses_allowed.collect(&:id).include?(s.to_i)
523 531 self.status_id = s
524 532 end
525 533 end
526 534 if new_record? && !statuses_allowed.include?(status)
527 535 self.status = statuses_allowed.first || default_status
528 536 end
529 537 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
530 538 if u.blank?
531 539 self.assigned_to_id = nil
532 540 else
533 541 u = u.to_i
534 542 if assignable_users.any?{|assignable_user| assignable_user.id == u}
535 543 self.assigned_to_id = u
536 544 end
537 545 end
538 546 end
539 547
540 548
541 549 attrs = delete_unsafe_attributes(attrs, user)
542 550 return if attrs.empty?
543 551
544 552 if attrs['parent_issue_id'].present?
545 553 s = attrs['parent_issue_id'].to_s
546 554 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
547 555 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
548 556 end
549 557 end
550 558
551 559 if attrs['custom_field_values'].present?
552 560 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
553 561 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
554 562 end
555 563
556 564 if attrs['custom_fields'].present?
557 565 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
558 566 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
559 567 end
560 568
561 569 # mass-assignment security bypass
562 570 assign_attributes attrs, :without_protection => true
563 571 end
564 572
565 573 def disabled_core_fields
566 574 tracker ? tracker.disabled_core_fields : []
567 575 end
568 576
569 577 # Returns the custom_field_values that can be edited by the given user
570 578 def editable_custom_field_values(user=nil)
571 579 visible_custom_field_values(user).reject do |value|
572 580 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
573 581 end
574 582 end
575 583
576 584 # Returns the custom fields that can be edited by the given user
577 585 def editable_custom_fields(user=nil)
578 586 editable_custom_field_values(user).map(&:custom_field).uniq
579 587 end
580 588
581 589 # Returns the names of attributes that are read-only for user or the current user
582 590 # For users with multiple roles, the read-only fields are the intersection of
583 591 # read-only fields of each role
584 592 # The result is an array of strings where sustom fields are represented with their ids
585 593 #
586 594 # Examples:
587 595 # issue.read_only_attribute_names # => ['due_date', '2']
588 596 # issue.read_only_attribute_names(user) # => []
589 597 def read_only_attribute_names(user=nil)
590 598 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
591 599 end
592 600
593 601 # Returns the names of required attributes for user or the current user
594 602 # For users with multiple roles, the required fields are the intersection of
595 603 # required fields of each role
596 604 # The result is an array of strings where sustom fields are represented with their ids
597 605 #
598 606 # Examples:
599 607 # issue.required_attribute_names # => ['due_date', '2']
600 608 # issue.required_attribute_names(user) # => []
601 609 def required_attribute_names(user=nil)
602 610 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
603 611 end
604 612
605 613 # Returns true if the attribute is required for user
606 614 def required_attribute?(name, user=nil)
607 615 required_attribute_names(user).include?(name.to_s)
608 616 end
609 617
610 618 # Returns a hash of the workflow rule by attribute for the given user
611 619 #
612 620 # Examples:
613 621 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
614 622 def workflow_rule_by_attribute(user=nil)
615 623 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
616 624
617 625 user_real = user || User.current
618 626 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
619 627 roles = roles.select(&:consider_workflow?)
620 628 return {} if roles.empty?
621 629
622 630 result = {}
623 631 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
624 632 if workflow_permissions.any?
625 633 workflow_rules = workflow_permissions.inject({}) do |h, wp|
626 634 h[wp.field_name] ||= {}
627 635 h[wp.field_name][wp.role_id] = wp.rule
628 636 h
629 637 end
630 638 fields_with_roles = {}
631 639 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
632 640 fields_with_roles[field_id] ||= []
633 641 fields_with_roles[field_id] << role_id
634 642 end
635 643 roles.each do |role|
636 644 fields_with_roles.each do |field_id, role_ids|
637 645 unless role_ids.include?(role.id)
638 646 field_name = field_id.to_s
639 647 workflow_rules[field_name] ||= {}
640 648 workflow_rules[field_name][role.id] = 'readonly'
641 649 end
642 650 end
643 651 end
644 652 workflow_rules.each do |attr, rules|
645 653 next if rules.size < roles.size
646 654 uniq_rules = rules.values.uniq
647 655 if uniq_rules.size == 1
648 656 result[attr] = uniq_rules.first
649 657 else
650 658 result[attr] = 'required'
651 659 end
652 660 end
653 661 end
654 662 @workflow_rule_by_attribute = result if user.nil?
655 663 result
656 664 end
657 665 private :workflow_rule_by_attribute
658 666
659 667 def done_ratio
660 668 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
661 669 status.default_done_ratio
662 670 else
663 671 read_attribute(:done_ratio)
664 672 end
665 673 end
666 674
667 675 def self.use_status_for_done_ratio?
668 676 Setting.issue_done_ratio == 'issue_status'
669 677 end
670 678
671 679 def self.use_field_for_done_ratio?
672 680 Setting.issue_done_ratio == 'issue_field'
673 681 end
674 682
675 683 def validate_issue
676 684 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
677 685 errors.add :due_date, :greater_than_start_date
678 686 end
679 687
680 688 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
681 689 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
682 690 end
683 691
684 692 if fixed_version
685 693 if !assignable_versions.include?(fixed_version)
686 694 errors.add :fixed_version_id, :inclusion
687 695 elsif reopening? && fixed_version.closed?
688 696 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
689 697 end
690 698 end
691 699
692 700 # Checks that the issue can not be added/moved to a disabled tracker
693 701 if project && (tracker_id_changed? || project_id_changed?)
694 702 if tracker && !project.trackers.include?(tracker)
695 703 errors.add :tracker_id, :inclusion
696 704 end
697 705 end
698 706
699 707 # Checks parent issue assignment
700 708 if @invalid_parent_issue_id.present?
701 709 errors.add :parent_issue_id, :invalid
702 710 elsif @parent_issue
703 711 if !valid_parent_project?(@parent_issue)
704 712 errors.add :parent_issue_id, :invalid
705 713 elsif (@parent_issue != parent) && (
706 714 self.would_reschedule?(@parent_issue) ||
707 715 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
708 716 )
709 717 errors.add :parent_issue_id, :invalid
710 718 elsif !new_record?
711 719 # moving an existing issue
712 720 if move_possible?(@parent_issue)
713 721 # move accepted
714 722 else
715 723 errors.add :parent_issue_id, :invalid
716 724 end
717 725 end
718 726 end
719 727 end
720 728
721 729 # Validates the issue against additional workflow requirements
722 730 def validate_required_fields
723 731 user = new_record? ? author : current_journal.try(:user)
724 732
725 733 required_attribute_names(user).each do |attribute|
726 734 if attribute =~ /^\d+$/
727 735 attribute = attribute.to_i
728 736 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
729 737 if v && Array(v.value).detect(&:present?).nil?
730 738 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
731 739 end
732 740 else
733 741 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
734 742 next if attribute == 'category_id' && project.try(:issue_categories).blank?
735 743 next if attribute == 'fixed_version_id' && assignable_versions.blank?
736 744 errors.add attribute, :blank
737 745 end
738 746 end
739 747 end
740 748 end
741 749
742 750 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
743 751 # so that custom values that are not editable are not validated (eg. a custom field that
744 752 # is marked as required should not trigger a validation error if the user is not allowed
745 753 # to edit this field).
746 754 def validate_custom_field_values
747 755 user = new_record? ? author : current_journal.try(:user)
748 756 if new_record? || custom_field_values_changed?
749 757 editable_custom_field_values(user).each(&:validate_value)
750 758 end
751 759 end
752 760
753 761 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
754 762 # even if the user turns off the setting later
755 763 def update_done_ratio_from_issue_status
756 764 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
757 765 self.done_ratio = status.default_done_ratio
758 766 end
759 767 end
760 768
761 769 def init_journal(user, notes = "")
762 770 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
763 771 end
764 772
765 773 # Returns the current journal or nil if it's not initialized
766 774 def current_journal
767 775 @current_journal
768 776 end
769 777
770 778 # Returns the names of attributes that are journalized when updating the issue
771 779 def journalized_attribute_names
772 780 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
773 781 if tracker
774 782 names -= tracker.disabled_core_fields
775 783 end
776 784 names
777 785 end
778 786
779 787 # Returns the id of the last journal or nil
780 788 def last_journal_id
781 789 if new_record?
782 790 nil
783 791 else
784 792 journals.maximum(:id)
785 793 end
786 794 end
787 795
788 796 # Returns a scope for journals that have an id greater than journal_id
789 797 def journals_after(journal_id)
790 798 scope = journals.reorder("#{Journal.table_name}.id ASC")
791 799 if journal_id.present?
792 800 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
793 801 end
794 802 scope
795 803 end
796 804
797 805 # Returns the initial status of the issue
798 806 # Returns nil for a new issue
799 807 def status_was
800 808 if status_id_changed?
801 809 if status_id_was.to_i > 0
802 810 @status_was ||= IssueStatus.find_by_id(status_id_was)
803 811 end
804 812 else
805 813 @status_was ||= status
806 814 end
807 815 end
808 816
809 817 # Return true if the issue is closed, otherwise false
810 818 def closed?
811 819 status.present? && status.is_closed?
812 820 end
813 821
814 822 # Returns true if the issue was closed when loaded
815 823 def was_closed?
816 824 status_was.present? && status_was.is_closed?
817 825 end
818 826
819 827 # Return true if the issue is being reopened
820 828 def reopening?
821 829 if new_record?
822 830 false
823 831 else
824 832 status_id_changed? && !closed? && was_closed?
825 833 end
826 834 end
827 835 alias :reopened? :reopening?
828 836
829 837 # Return true if the issue is being closed
830 838 def closing?
831 839 if new_record?
832 840 closed?
833 841 else
834 842 status_id_changed? && closed? && !was_closed?
835 843 end
836 844 end
837 845
838 846 # Returns true if the issue is overdue
839 847 def overdue?
840 848 due_date.present? && (due_date < User.current.today) && !closed?
841 849 end
842 850
843 851 # Is the amount of work done less than it should for the due date
844 852 def behind_schedule?
845 853 return false if start_date.nil? || due_date.nil?
846 854 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
847 855 return done_date <= User.current.today
848 856 end
849 857
850 858 # Does this issue have children?
851 859 def children?
852 860 !leaf?
853 861 end
854 862
855 863 # Users the issue can be assigned to
856 864 def assignable_users
857 865 users = project.assignable_users(tracker).to_a
858 866 users << author if author && author.active?
859 867 users << assigned_to if assigned_to
860 868 users.uniq.sort
861 869 end
862 870
863 871 # Versions that the issue can be assigned to
864 872 def assignable_versions
865 873 return @assignable_versions if @assignable_versions
866 874
867 875 versions = project.shared_versions.open.to_a
868 876 if fixed_version
869 877 if fixed_version_id_changed?
870 878 # nothing to do
871 879 elsif project_id_changed?
872 880 if project.shared_versions.include?(fixed_version)
873 881 versions << fixed_version
874 882 end
875 883 else
876 884 versions << fixed_version
877 885 end
878 886 end
879 887 @assignable_versions = versions.uniq.sort
880 888 end
881 889
882 890 # Returns true if this issue is blocked by another issue that is still open
883 891 def blocked?
884 892 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
885 893 end
886 894
887 895 # Returns the default status of the issue based on its tracker
888 896 # Returns nil if tracker is nil
889 897 def default_status
890 898 tracker.try(:default_status)
891 899 end
892 900
893 901 # Returns an array of statuses that user is able to apply
894 902 def new_statuses_allowed_to(user=User.current, include_default=false)
895 903 if new_record? && @copied_from
896 904 [default_status, @copied_from.status].compact.uniq.sort
897 905 else
898 906 initial_status = nil
899 907 if new_record?
900 908 # nop
901 909 elsif tracker_id_changed?
902 910 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
903 911 initial_status = default_status
904 912 elsif tracker.issue_status_ids.include?(status_id_was)
905 913 initial_status = IssueStatus.find_by_id(status_id_was)
906 914 else
907 915 initial_status = default_status
908 916 end
909 917 else
910 918 initial_status = status_was
911 919 end
912 920
913 921 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
914 922 assignee_transitions_allowed = initial_assigned_to_id.present? &&
915 923 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
916 924
917 925 statuses = []
918 926 statuses += IssueStatus.new_statuses_allowed(
919 927 initial_status,
920 928 user.admin ? Role.all.to_a : user.roles_for_project(project),
921 929 tracker,
922 930 author == user,
923 931 assignee_transitions_allowed
924 932 )
925 933 statuses << initial_status unless statuses.empty?
926 934 statuses << default_status if include_default || (new_record? && statuses.empty?)
927 935 statuses = statuses.compact.uniq.sort
928 936 if blocked?
929 937 statuses.reject!(&:is_closed?)
930 938 end
931 939 statuses
932 940 end
933 941 end
934 942
935 943 # Returns the previous assignee (user or group) if changed
936 944 def assigned_to_was
937 945 # assigned_to_id_was is reset before after_save callbacks
938 946 user_id = @previous_assigned_to_id || assigned_to_id_was
939 947 if user_id && user_id != assigned_to_id
940 948 @assigned_to_was ||= Principal.find_by_id(user_id)
941 949 end
942 950 end
943 951
944 952 # Returns the original tracker
945 953 def tracker_was
946 954 Tracker.find_by_id(tracker_id_was)
947 955 end
948 956
949 957 # Returns the users that should be notified
950 958 def notified_users
951 959 notified = []
952 960 # Author and assignee are always notified unless they have been
953 961 # locked or don't want to be notified
954 962 notified << author if author
955 963 if assigned_to
956 964 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
957 965 end
958 966 if assigned_to_was
959 967 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
960 968 end
961 969 notified = notified.select {|u| u.active? && u.notify_about?(self)}
962 970
963 971 notified += project.notified_users
964 972 notified.uniq!
965 973 # Remove users that can not view the issue
966 974 notified.reject! {|user| !visible?(user)}
967 975 notified
968 976 end
969 977
970 978 # Returns the email addresses that should be notified
971 979 def recipients
972 980 notified_users.collect(&:mail)
973 981 end
974 982
975 983 def each_notification(users, &block)
976 984 if users.any?
977 985 if custom_field_values.detect {|value| !value.custom_field.visible?}
978 986 users_by_custom_field_visibility = users.group_by do |user|
979 987 visible_custom_field_values(user).map(&:custom_field_id).sort
980 988 end
981 989 users_by_custom_field_visibility.values.each do |users|
982 990 yield(users)
983 991 end
984 992 else
985 993 yield(users)
986 994 end
987 995 end
988 996 end
989 997
990 998 def notify?
991 999 @notify != false
992 1000 end
993 1001
994 1002 def notify=(arg)
995 1003 @notify = arg
996 1004 end
997 1005
998 1006 # Returns the number of hours spent on this issue
999 1007 def spent_hours
1000 1008 @spent_hours ||= time_entries.sum(:hours) || 0
1001 1009 end
1002 1010
1003 1011 # Returns the total number of hours spent on this issue and its descendants
1004 1012 def total_spent_hours
1005 1013 @total_spent_hours ||= if leaf?
1006 1014 spent_hours
1007 1015 else
1008 1016 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1009 1017 end
1010 1018 end
1011 1019
1012 1020 def total_estimated_hours
1013 1021 if leaf?
1014 1022 estimated_hours
1015 1023 else
1016 1024 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1017 1025 end
1018 1026 end
1019 1027
1020 1028 def relations
1021 1029 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1022 1030 end
1023 1031
1024 1032 # Preloads relations for a collection of issues
1025 1033 def self.load_relations(issues)
1026 1034 if issues.any?
1027 1035 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1028 1036 issues.each do |issue|
1029 1037 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1030 1038 end
1031 1039 end
1032 1040 end
1033 1041
1034 1042 # Preloads visible spent time for a collection of issues
1035 1043 def self.load_visible_spent_hours(issues, user=User.current)
1036 1044 if issues.any?
1037 1045 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1038 1046 issues.each do |issue|
1039 1047 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1040 1048 end
1041 1049 end
1042 1050 end
1043 1051
1044 1052 # Preloads visible total spent time for a collection of issues
1045 1053 def self.load_visible_total_spent_hours(issues, user=User.current)
1046 1054 if issues.any?
1047 1055 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1048 1056 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1049 1057 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1050 1058 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1051 1059 issues.each do |issue|
1052 1060 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1053 1061 end
1054 1062 end
1055 1063 end
1056 1064
1057 1065 # Preloads visible relations for a collection of issues
1058 1066 def self.load_visible_relations(issues, user=User.current)
1059 1067 if issues.any?
1060 1068 issue_ids = issues.map(&:id)
1061 1069 # Relations with issue_from in given issues and visible issue_to
1062 1070 relations_from = IssueRelation.joins(:issue_to => :project).
1063 1071 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1064 1072 # Relations with issue_to in given issues and visible issue_from
1065 1073 relations_to = IssueRelation.joins(:issue_from => :project).
1066 1074 where(visible_condition(user)).
1067 1075 where(:issue_to_id => issue_ids).to_a
1068 1076 issues.each do |issue|
1069 1077 relations =
1070 1078 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1071 1079 relations_to.select {|relation| relation.issue_to_id == issue.id}
1072 1080
1073 1081 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1074 1082 end
1075 1083 end
1076 1084 end
1077 1085
1078 1086 # Finds an issue relation given its id.
1079 1087 def find_relation(relation_id)
1080 1088 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1081 1089 end
1082 1090
1083 1091 # Returns true if this issue blocks the other issue, otherwise returns false
1084 1092 def blocks?(other)
1085 1093 all = [self]
1086 1094 last = [self]
1087 1095 while last.any?
1088 1096 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1089 1097 current -= last
1090 1098 current -= all
1091 1099 return true if current.include?(other)
1092 1100 last = current
1093 1101 all += last
1094 1102 end
1095 1103 false
1096 1104 end
1097 1105
1098 1106 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1099 1107 def would_reschedule?(other)
1100 1108 all = [self]
1101 1109 last = [self]
1102 1110 while last.any?
1103 1111 current = last.map {|i|
1104 1112 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1105 1113 i.leaves.to_a +
1106 1114 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1107 1115 }.flatten.uniq
1108 1116 current -= last
1109 1117 current -= all
1110 1118 return true if current.include?(other)
1111 1119 last = current
1112 1120 all += last
1113 1121 end
1114 1122 false
1115 1123 end
1116 1124
1117 1125 # Returns an array of issues that duplicate this one
1118 1126 def duplicates
1119 1127 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1120 1128 end
1121 1129
1122 1130 # Returns the due date or the target due date if any
1123 1131 # Used on gantt chart
1124 1132 def due_before
1125 1133 due_date || (fixed_version ? fixed_version.effective_date : nil)
1126 1134 end
1127 1135
1128 1136 # Returns the time scheduled for this issue.
1129 1137 #
1130 1138 # Example:
1131 1139 # Start Date: 2/26/09, End Date: 3/04/09
1132 1140 # duration => 6
1133 1141 def duration
1134 1142 (start_date && due_date) ? due_date - start_date : 0
1135 1143 end
1136 1144
1137 1145 # Returns the duration in working days
1138 1146 def working_duration
1139 1147 (start_date && due_date) ? working_days(start_date, due_date) : 0
1140 1148 end
1141 1149
1142 1150 def soonest_start(reload=false)
1143 1151 if @soonest_start.nil? || reload
1144 1152 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1145 1153 p = @parent_issue || parent
1146 1154 if p && Setting.parent_issue_dates == 'derived'
1147 1155 dates << p.soonest_start
1148 1156 end
1149 1157 @soonest_start = dates.compact.max
1150 1158 end
1151 1159 @soonest_start
1152 1160 end
1153 1161
1154 1162 # Sets start_date on the given date or the next working day
1155 1163 # and changes due_date to keep the same working duration.
1156 1164 def reschedule_on(date)
1157 1165 wd = working_duration
1158 1166 date = next_working_date(date)
1159 1167 self.start_date = date
1160 1168 self.due_date = add_working_days(date, wd)
1161 1169 end
1162 1170
1163 1171 # Reschedules the issue on the given date or the next working day and saves the record.
1164 1172 # If the issue is a parent task, this is done by rescheduling its subtasks.
1165 1173 def reschedule_on!(date)
1166 1174 return if date.nil?
1167 1175 if leaf? || !dates_derived?
1168 1176 if start_date.nil? || start_date != date
1169 1177 if start_date && start_date > date
1170 1178 # Issue can not be moved earlier than its soonest start date
1171 1179 date = [soonest_start(true), date].compact.max
1172 1180 end
1173 1181 reschedule_on(date)
1174 1182 begin
1175 1183 save
1176 1184 rescue ActiveRecord::StaleObjectError
1177 1185 reload
1178 1186 reschedule_on(date)
1179 1187 save
1180 1188 end
1181 1189 end
1182 1190 else
1183 1191 leaves.each do |leaf|
1184 1192 if leaf.start_date
1185 1193 # Only move subtask if it starts at the same date as the parent
1186 1194 # or if it starts before the given date
1187 1195 if start_date == leaf.start_date || date > leaf.start_date
1188 1196 leaf.reschedule_on!(date)
1189 1197 end
1190 1198 else
1191 1199 leaf.reschedule_on!(date)
1192 1200 end
1193 1201 end
1194 1202 end
1195 1203 end
1196 1204
1197 1205 def dates_derived?
1198 1206 !leaf? && Setting.parent_issue_dates == 'derived'
1199 1207 end
1200 1208
1201 1209 def priority_derived?
1202 1210 !leaf? && Setting.parent_issue_priority == 'derived'
1203 1211 end
1204 1212
1205 1213 def done_ratio_derived?
1206 1214 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1207 1215 end
1208 1216
1209 1217 def <=>(issue)
1210 1218 if issue.nil?
1211 1219 -1
1212 1220 elsif root_id != issue.root_id
1213 1221 (root_id || 0) <=> (issue.root_id || 0)
1214 1222 else
1215 1223 (lft || 0) <=> (issue.lft || 0)
1216 1224 end
1217 1225 end
1218 1226
1219 1227 def to_s
1220 1228 "#{tracker} ##{id}: #{subject}"
1221 1229 end
1222 1230
1223 1231 # Returns a string of css classes that apply to the issue
1224 1232 def css_classes(user=User.current)
1225 1233 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1226 1234 s << ' closed' if closed?
1227 1235 s << ' overdue' if overdue?
1228 1236 s << ' child' if child?
1229 1237 s << ' parent' unless leaf?
1230 1238 s << ' private' if is_private?
1231 1239 if user.logged?
1232 1240 s << ' created-by-me' if author_id == user.id
1233 1241 s << ' assigned-to-me' if assigned_to_id == user.id
1234 1242 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1235 1243 end
1236 1244 s
1237 1245 end
1238 1246
1239 1247 # Unassigns issues from +version+ if it's no longer shared with issue's project
1240 1248 def self.update_versions_from_sharing_change(version)
1241 1249 # Update issues assigned to the version
1242 1250 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1243 1251 end
1244 1252
1245 1253 # Unassigns issues from versions that are no longer shared
1246 1254 # after +project+ was moved
1247 1255 def self.update_versions_from_hierarchy_change(project)
1248 1256 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1249 1257 # Update issues of the moved projects and issues assigned to a version of a moved project
1250 1258 Issue.update_versions(
1251 1259 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1252 1260 moved_project_ids, moved_project_ids]
1253 1261 )
1254 1262 end
1255 1263
1256 1264 def parent_issue_id=(arg)
1257 1265 s = arg.to_s.strip.presence
1258 1266 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1259 1267 @invalid_parent_issue_id = nil
1260 1268 elsif s.blank?
1261 1269 @parent_issue = nil
1262 1270 @invalid_parent_issue_id = nil
1263 1271 else
1264 1272 @parent_issue = nil
1265 1273 @invalid_parent_issue_id = arg
1266 1274 end
1267 1275 end
1268 1276
1269 1277 def parent_issue_id
1270 1278 if @invalid_parent_issue_id
1271 1279 @invalid_parent_issue_id
1272 1280 elsif instance_variable_defined? :@parent_issue
1273 1281 @parent_issue.nil? ? nil : @parent_issue.id
1274 1282 else
1275 1283 parent_id
1276 1284 end
1277 1285 end
1278 1286
1279 1287 def set_parent_id
1280 1288 self.parent_id = parent_issue_id
1281 1289 end
1282 1290
1283 1291 # Returns true if issue's project is a valid
1284 1292 # parent issue project
1285 1293 def valid_parent_project?(issue=parent)
1286 1294 return true if issue.nil? || issue.project_id == project_id
1287 1295
1288 1296 case Setting.cross_project_subtasks
1289 1297 when 'system'
1290 1298 true
1291 1299 when 'tree'
1292 1300 issue.project.root == project.root
1293 1301 when 'hierarchy'
1294 1302 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1295 1303 when 'descendants'
1296 1304 issue.project.is_or_is_ancestor_of?(project)
1297 1305 else
1298 1306 false
1299 1307 end
1300 1308 end
1301 1309
1302 1310 # Returns an issue scope based on project and scope
1303 1311 def self.cross_project_scope(project, scope=nil)
1304 1312 if project.nil?
1305 1313 return Issue
1306 1314 end
1307 1315 case scope
1308 1316 when 'all', 'system'
1309 1317 Issue
1310 1318 when 'tree'
1311 1319 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1312 1320 :lft => project.root.lft, :rgt => project.root.rgt)
1313 1321 when 'hierarchy'
1314 1322 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1315 1323 :lft => project.lft, :rgt => project.rgt)
1316 1324 when 'descendants'
1317 1325 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1318 1326 :lft => project.lft, :rgt => project.rgt)
1319 1327 else
1320 1328 Issue.where(:project_id => project.id)
1321 1329 end
1322 1330 end
1323 1331
1324 1332 def self.by_tracker(project)
1325 1333 count_and_group_by(:project => project, :association => :tracker)
1326 1334 end
1327 1335
1328 1336 def self.by_version(project)
1329 1337 count_and_group_by(:project => project, :association => :fixed_version)
1330 1338 end
1331 1339
1332 1340 def self.by_priority(project)
1333 1341 count_and_group_by(:project => project, :association => :priority)
1334 1342 end
1335 1343
1336 1344 def self.by_category(project)
1337 1345 count_and_group_by(:project => project, :association => :category)
1338 1346 end
1339 1347
1340 1348 def self.by_assigned_to(project)
1341 1349 count_and_group_by(:project => project, :association => :assigned_to)
1342 1350 end
1343 1351
1344 1352 def self.by_author(project)
1345 1353 count_and_group_by(:project => project, :association => :author)
1346 1354 end
1347 1355
1348 1356 def self.by_subproject(project)
1349 1357 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1350 1358 r.reject {|r| r["project_id"] == project.id.to_s}
1351 1359 end
1352 1360
1353 1361 # Query generator for selecting groups of issue counts for a project
1354 1362 # based on specific criteria
1355 1363 #
1356 1364 # Options
1357 1365 # * project - Project to search in.
1358 1366 # * with_subprojects - Includes subprojects issues if set to true.
1359 1367 # * association - Symbol. Association for grouping.
1360 1368 def self.count_and_group_by(options)
1361 1369 assoc = reflect_on_association(options[:association])
1362 1370 select_field = assoc.foreign_key
1363 1371
1364 1372 Issue.
1365 1373 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1366 1374 joins(:status, assoc.name).
1367 1375 group(:status_id, :is_closed, select_field).
1368 1376 count.
1369 1377 map do |columns, total|
1370 1378 status_id, is_closed, field_value = columns
1371 1379 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1372 1380 {
1373 1381 "status_id" => status_id.to_s,
1374 1382 "closed" => is_closed,
1375 1383 select_field => field_value.to_s,
1376 1384 "total" => total.to_s
1377 1385 }
1378 1386 end
1379 1387 end
1380 1388
1381 1389 # Returns a scope of projects that user can assign the issue to
1382 1390 def allowed_target_projects(user=User.current)
1383 1391 current_project = new_record? ? nil : project
1384 1392 self.class.allowed_target_projects(user, current_project)
1385 1393 end
1386 1394
1387 1395 # Returns a scope of projects that user can assign issues to
1388 1396 # If current_project is given, it will be included in the scope
1389 1397 def self.allowed_target_projects(user=User.current, current_project=nil)
1390 1398 condition = Project.allowed_to_condition(user, :add_issues)
1391 1399 if current_project
1392 1400 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1393 1401 end
1394 1402 Project.where(condition).having_trackers
1395 1403 end
1396 1404
1397 1405 # Returns a scope of trackers that user can assign the issue to
1398 1406 def allowed_target_trackers(user=User.current)
1399 1407 self.class.allowed_target_trackers(project, user, tracker_id_was)
1400 1408 end
1401 1409
1402 1410 # Returns a scope of trackers that user can assign project issues to
1403 1411 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1404 1412 if project
1405 1413 scope = project.trackers.sorted
1406 1414 unless user.admin?
1407 1415 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1408 1416 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1409 1417 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1410 1418 if current_tracker
1411 1419 tracker_ids << current_tracker
1412 1420 end
1413 1421 scope = scope.where(:id => tracker_ids)
1414 1422 end
1415 1423 end
1416 1424 scope
1417 1425 else
1418 1426 Tracker.none
1419 1427 end
1420 1428 end
1421 1429
1422 1430 private
1423 1431
1424 1432 def user_tracker_permission?(user, permission)
1425 1433 if user.admin?
1426 1434 true
1427 1435 else
1428 1436 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1429 1437 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1430 1438 end
1431 1439 end
1432 1440
1433 1441 def after_project_change
1434 1442 # Update project_id on related time entries
1435 1443 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1436 1444
1437 1445 # Delete issue relations
1438 1446 unless Setting.cross_project_issue_relations?
1439 1447 relations_from.clear
1440 1448 relations_to.clear
1441 1449 end
1442 1450
1443 1451 # Move subtasks that were in the same project
1444 1452 children.each do |child|
1445 1453 next unless child.project_id == project_id_was
1446 1454 # Change project and keep project
1447 1455 child.send :project=, project, true
1448 1456 unless child.save
1449 1457 raise ActiveRecord::Rollback
1450 1458 end
1451 1459 end
1452 1460 end
1453 1461
1454 1462 # Callback for after the creation of an issue by copy
1455 1463 # * adds a "copied to" relation with the copied issue
1456 1464 # * copies subtasks from the copied issue
1457 1465 def after_create_from_copy
1458 1466 return unless copy? && !@after_create_from_copy_handled
1459 1467
1460 1468 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1461 1469 if @current_journal
1462 1470 @copied_from.init_journal(@current_journal.user)
1463 1471 end
1464 1472 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1465 1473 unless relation.save
1466 1474 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1467 1475 end
1468 1476 end
1469 1477
1470 1478 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1471 1479 copy_options = (@copy_options || {}).merge(:subtasks => false)
1472 1480 copied_issue_ids = {@copied_from.id => self.id}
1473 1481 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1474 1482 # Do not copy self when copying an issue as a descendant of the copied issue
1475 1483 next if child == self
1476 1484 # Do not copy subtasks of issues that were not copied
1477 1485 next unless copied_issue_ids[child.parent_id]
1478 1486 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1479 1487 unless child.visible?
1480 1488 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1481 1489 next
1482 1490 end
1483 1491 copy = Issue.new.copy_from(child, copy_options)
1484 1492 if @current_journal
1485 1493 copy.init_journal(@current_journal.user)
1486 1494 end
1487 1495 copy.author = author
1488 1496 copy.project = project
1489 1497 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1490 1498 unless copy.save
1491 1499 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1492 1500 next
1493 1501 end
1494 1502 copied_issue_ids[child.id] = copy.id
1495 1503 end
1496 1504 end
1497 1505 @after_create_from_copy_handled = true
1498 1506 end
1499 1507
1500 1508 def update_nested_set_attributes
1501 1509 if parent_id_changed?
1502 1510 update_nested_set_attributes_on_parent_change
1503 1511 end
1504 1512 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1505 1513 end
1506 1514
1507 1515 # Updates the nested set for when an existing issue is moved
1508 1516 def update_nested_set_attributes_on_parent_change
1509 1517 former_parent_id = parent_id_was
1510 1518 # delete invalid relations of all descendants
1511 1519 self_and_descendants.each do |issue|
1512 1520 issue.relations.each do |relation|
1513 1521 relation.destroy unless relation.valid?
1514 1522 end
1515 1523 end
1516 1524 # update former parent
1517 1525 recalculate_attributes_for(former_parent_id) if former_parent_id
1518 1526 end
1519 1527
1520 1528 def update_parent_attributes
1521 1529 if parent_id
1522 1530 recalculate_attributes_for(parent_id)
1523 1531 association(:parent).reset
1524 1532 end
1525 1533 end
1526 1534
1527 1535 def recalculate_attributes_for(issue_id)
1528 1536 if issue_id && p = Issue.find_by_id(issue_id)
1529 1537 if p.priority_derived?
1530 1538 # priority = highest priority of open children
1531 1539 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1532 1540 p.priority = IssuePriority.find_by_position(priority_position)
1533 1541 else
1534 1542 p.priority = IssuePriority.default
1535 1543 end
1536 1544 end
1537 1545
1538 1546 if p.dates_derived?
1539 1547 # start/due dates = lowest/highest dates of children
1540 1548 p.start_date = p.children.minimum(:start_date)
1541 1549 p.due_date = p.children.maximum(:due_date)
1542 1550 if p.start_date && p.due_date && p.due_date < p.start_date
1543 1551 p.start_date, p.due_date = p.due_date, p.start_date
1544 1552 end
1545 1553 end
1546 1554
1547 1555 if p.done_ratio_derived?
1548 1556 # done ratio = weighted average ratio of leaves
1549 1557 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1550 1558 child_count = p.children.count
1551 1559 if child_count > 0
1552 1560 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1553 1561 if average == 0
1554 1562 average = 1
1555 1563 end
1556 1564 done = p.children.joins(:status).
1557 1565 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1558 1566 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1559 1567 progress = done / (average * child_count)
1560 1568 p.done_ratio = progress.round
1561 1569 end
1562 1570 end
1563 1571 end
1564 1572
1565 1573 # ancestors will be recursively updated
1566 1574 p.save(:validate => false)
1567 1575 end
1568 1576 end
1569 1577
1570 1578 # Update issues so their versions are not pointing to a
1571 1579 # fixed_version that is not shared with the issue's project
1572 1580 def self.update_versions(conditions=nil)
1573 1581 # Only need to update issues with a fixed_version from
1574 1582 # a different project and that is not systemwide shared
1575 1583 Issue.joins(:project, :fixed_version).
1576 1584 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1577 1585 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1578 1586 " AND #{Version.table_name}.sharing <> 'system'").
1579 1587 where(conditions).each do |issue|
1580 1588 next if issue.project.nil? || issue.fixed_version.nil?
1581 1589 unless issue.project.shared_versions.include?(issue.fixed_version)
1582 1590 issue.init_journal(User.current)
1583 1591 issue.fixed_version = nil
1584 1592 issue.save
1585 1593 end
1586 1594 end
1587 1595 end
1588 1596
1597 def delete_selected_attachments
1598 if deleted_attachment_ids.present?
1599 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1600 attachments.delete(objects)
1601 end
1602 end
1603
1589 1604 # Callback on file attachment
1590 1605 def attachment_added(attachment)
1591 1606 if current_journal && !attachment.new_record?
1592 1607 current_journal.journalize_attachment(attachment, :added)
1593 1608 end
1594 1609 end
1595 1610
1596 1611 # Callback on attachment deletion
1597 1612 def attachment_removed(attachment)
1598 1613 if current_journal && !attachment.new_record?
1599 1614 current_journal.journalize_attachment(attachment, :removed)
1600 1615 current_journal.save
1601 1616 end
1602 1617 end
1603 1618
1604 1619 # Called after a relation is added
1605 1620 def relation_added(relation)
1606 1621 if current_journal
1607 1622 current_journal.journalize_relation(relation, :added)
1608 1623 current_journal.save
1609 1624 end
1610 1625 end
1611 1626
1612 1627 # Called after a relation is removed
1613 1628 def relation_removed(relation)
1614 1629 if current_journal
1615 1630 current_journal.journalize_relation(relation, :removed)
1616 1631 current_journal.save
1617 1632 end
1618 1633 end
1619 1634
1620 1635 # Default assignment based on category
1621 1636 def default_assign
1622 1637 if assigned_to.nil? && category && category.assigned_to
1623 1638 self.assigned_to = category.assigned_to
1624 1639 end
1625 1640 end
1626 1641
1627 1642 # Updates start/due dates of following issues
1628 1643 def reschedule_following_issues
1629 1644 if start_date_changed? || due_date_changed?
1630 1645 relations_from.each do |relation|
1631 1646 relation.set_issue_to_dates
1632 1647 end
1633 1648 end
1634 1649 end
1635 1650
1636 1651 # Closes duplicates if the issue is being closed
1637 1652 def close_duplicates
1638 1653 if closing?
1639 1654 duplicates.each do |duplicate|
1640 1655 # Reload is needed in case the duplicate was updated by a previous duplicate
1641 1656 duplicate.reload
1642 1657 # Don't re-close it if it's already closed
1643 1658 next if duplicate.closed?
1644 1659 # Same user and notes
1645 1660 if @current_journal
1646 1661 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1647 1662 duplicate.private_notes = @current_journal.private_notes
1648 1663 end
1649 1664 duplicate.update_attribute :status, self.status
1650 1665 end
1651 1666 end
1652 1667 end
1653 1668
1654 1669 # Make sure updated_on is updated when adding a note and set updated_on now
1655 1670 # so we can set closed_on with the same value on closing
1656 1671 def force_updated_on_change
1657 1672 if @current_journal || changed?
1658 1673 self.updated_on = current_time_from_proper_timezone
1659 1674 if new_record?
1660 1675 self.created_on = updated_on
1661 1676 end
1662 1677 end
1663 1678 end
1664 1679
1665 1680 # Callback for setting closed_on when the issue is closed.
1666 1681 # The closed_on attribute stores the time of the last closing
1667 1682 # and is preserved when the issue is reopened.
1668 1683 def update_closed_on
1669 1684 if closing?
1670 1685 self.closed_on = updated_on
1671 1686 end
1672 1687 end
1673 1688
1674 1689 # Saves the changes in a Journal
1675 1690 # Called after_save
1676 1691 def create_journal
1677 1692 if current_journal
1678 1693 current_journal.save
1679 1694 end
1680 1695 end
1681 1696
1682 1697 def send_notification
1683 1698 if notify? && Setting.notified_events.include?('issue_added')
1684 1699 Mailer.deliver_issue_add(self)
1685 1700 end
1686 1701 end
1687 1702
1688 1703 # Stores the previous assignee so we can still have access
1689 1704 # to it during after_save callbacks (assigned_to_id_was is reset)
1690 1705 def set_assigned_to_was
1691 1706 @previous_assigned_to_id = assigned_to_id_was
1692 1707 end
1693 1708
1694 1709 # Clears the previous assignee at the end of after_save callbacks
1695 1710 def clear_assigned_to_was
1696 1711 @assigned_to_was = nil
1697 1712 @previous_assigned_to_id = nil
1698 1713 end
1699 1714
1700 1715 def clear_disabled_fields
1701 1716 if tracker
1702 1717 tracker.disabled_core_fields.each do |attribute|
1703 1718 send "#{attribute}=", nil
1704 1719 end
1705 1720 self.done_ratio ||= 0
1706 1721 end
1707 1722 end
1708 1723 end
@@ -1,60 +1,80
1 1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2 <%= error_messages_for 'issue', 'time_entry' %>
3 3 <%= render :partial => 'conflict' if @conflict %>
4 4 <div class="box">
5 5 <% if @issue.attributes_editable? %>
6 6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 7 <div id="all_attributes">
8 8 <%= render :partial => 'form', :locals => {:f => f} %>
9 9 </div>
10 10 </fieldset>
11 11 <% end %>
12 12 <% if User.current.allowed_to?(:log_time, @project) %>
13 13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 15 <div class="splitcontent">
16 16 <div class="splitcontentleft">
17 17 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
18 18 </div>
19 19 <div class="splitcontentright">
20 20 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
21 21 </div>
22 22 </div>
23 23 <p><%= time_entry.text_field :comments, :size => 60 %></p>
24 24 <% @time_entry.custom_field_values.each do |value| %>
25 25 <p><%= custom_field_tag_with_label :time_entry, value %></p>
26 26 <% end %>
27 27 <% end %>
28 28 </fieldset>
29 29 <% end %>
30 30 <% if @issue.notes_addable? %>
31 31 <fieldset><legend><%= l(:field_notes) %></legend>
32 32 <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
33 33 <%= wikitoolbar_for 'issue_notes' %>
34 34
35 35 <% if @issue.safe_attribute? 'private_notes' %>
36 36 <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
37 37 <% end %>
38 38
39 39 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
40 40 </fieldset>
41
41
42 42 <fieldset><legend><%= l(:label_attachment_plural) %></legend>
43 <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
43 <% if @issue.attachments.any? && @issue.safe_attribute?('deleted_attachment_ids') %>
44 <div class="contextual"><%= link_to l(:label_edit_attachments), '#', :onclick => "$('#existing-attachments').toggle(); return false;" %></div>
45 <div id="existing-attachments" style="<%= @issue.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
46 <% @issue.attachments.each do |attachment| %>
47 <span class="existing-attachment">
48 <%= text_field_tag '', attachment.filename, :class => "filename", :disabled => true %>
49 <label>
50 <%= check_box_tag 'issue[deleted_attachment_ids][]',
51 attachment.id,
52 @issue.deleted_attachment_ids.include?(attachment.id),
53 :id => nil, :class => "deleted_attachment" %> <%= l(:button_delete) %>
54 </label>
55 </span>
56 <% end %>
57 <hr />
58 </div>
59 <% end %>
60
61 <div id="new-attachments" style="display:inline-block;">
62 <%= render :partial => 'attachments/form', :locals => {:container => @issue} %>
63 </div>
44 64 </fieldset>
45 65 <% end %>
46 66 </div>
47 67
48 68 <%= f.hidden_field :lock_version %>
49 69 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
50 70 <%= submit_tag l(:button_submit) %>
51 71 <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
52 72 | <%= link_to l(:button_cancel), {}, :onclick => "$('#update').hide(); return false;" %>
53 73
54 74 <%= hidden_field_tag 'prev_issue_id', @prev_issue_id if @prev_issue_id %>
55 75 <%= hidden_field_tag 'next_issue_id', @next_issue_id if @next_issue_id %>
56 76 <%= hidden_field_tag 'issue_position', @issue_position if @issue_position %>
57 77 <%= hidden_field_tag 'issue_count', @issue_count if @issue_count %>
58 78 <% end %>
59 79
60 80 <div id="preview" class="wiki"></div>
@@ -1,191 +1,196
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3 3
4 4 function addFile(inputEl, file, eagerUpload) {
5 5
6 6 if ($('#attachments_fields').children().length < 10) {
7 7
8 8 var attachmentId = addFile.nextAttachmentId++;
9 9
10 10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11 11
12 12 fileSpan.append(
13 13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
14 14 $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
15 15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 16 ).appendTo('#attachments_fields');
17 17
18 18 if(eagerUpload) {
19 19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 20 }
21 21
22 22 return attachmentId;
23 23 }
24 24 return null;
25 25 }
26 26
27 27 addFile.nextAttachmentId = 1;
28 28
29 29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
30 30
31 31 function onLoadstart(e) {
32 32 fileSpan.removeClass('ajax-waiting');
33 33 fileSpan.addClass('ajax-loading');
34 34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
35 35 }
36 36
37 37 function onProgress(e) {
38 38 if(e.lengthComputable) {
39 39 this.progressbar( 'value', e.loaded * 100 / e.total );
40 40 }
41 41 }
42 42
43 43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
44 44
45 45 ajaxUpload.uploading++;
46 46
47 47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
48 48 loadstartEventHandler: onLoadstart.bind(progressSpan),
49 49 progressEventHandler: onProgress.bind(progressSpan)
50 50 })
51 51 .done(function(result) {
52 52 progressSpan.progressbar( 'value', 100 ).remove();
53 53 fileSpan.find('input.description, a').css('display', 'inline-block');
54 54 })
55 55 .fail(function(result) {
56 56 progressSpan.text(result.statusText);
57 57 }).always(function() {
58 58 ajaxUpload.uploading--;
59 59 fileSpan.removeClass('ajax-loading');
60 60 var form = fileSpan.parents('form');
61 61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
62 62 $('input:submit', form).removeAttr('disabled');
63 63 }
64 64 form.dequeue('upload');
65 65 });
66 66 }
67 67
68 68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
69 69 progressSpan.progressbar();
70 70 fileSpan.addClass('ajax-waiting');
71 71
72 72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
73 73
74 74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
75 75 actualUpload(file, attachmentId, fileSpan, inputEl);
76 76 else
77 77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
78 78 }
79 79
80 80 ajaxUpload.uploading = 0;
81 81
82 82 function removeFile() {
83 83 $(this).parent('span').remove();
84 84 return false;
85 85 }
86 86
87 87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
88 88
89 89 var actualOptions = $.extend({
90 90 loadstartEventHandler: $.noop,
91 91 progressEventHandler: $.noop
92 92 }, options);
93 93
94 94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
95 95 if (blob instanceof window.File) {
96 96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
97 97 uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
98 98 }
99 99
100 100 return $.ajax(uploadUrl, {
101 101 type: 'POST',
102 102 contentType: 'application/octet-stream',
103 103 beforeSend: function(jqXhr, settings) {
104 104 jqXhr.setRequestHeader('Accept', 'application/js');
105 105 // attach proper File object
106 106 settings.data = blob;
107 107 },
108 108 xhr: function() {
109 109 var xhr = $.ajaxSettings.xhr();
110 110 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
111 111 xhr.upload.onprogress = actualOptions.progressEventHandler;
112 112 return xhr;
113 113 },
114 114 data: blob,
115 115 cache: false,
116 116 processData: false
117 117 });
118 118 }
119 119
120 120 function addInputFiles(inputEl) {
121 121 var clearedFileInput = $(inputEl).clone().val('');
122 122
123 123 if ($.ajaxSettings.xhr().upload && inputEl.files) {
124 124 // upload files using ajax
125 125 uploadAndAttachFiles(inputEl.files, inputEl);
126 126 $(inputEl).remove();
127 127 } else {
128 128 // browser not supporting the file API, upload on form submission
129 129 var attachmentId;
130 130 var aFilename = inputEl.value.split(/\/|\\/);
131 131 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
132 132 if (attachmentId) {
133 133 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
134 134 }
135 135 }
136 136
137 137 clearedFileInput.insertAfter('#attachments_fields');
138 138 }
139 139
140 140 function uploadAndAttachFiles(files, inputEl) {
141 141
142 142 var maxFileSize = $(inputEl).data('max-file-size');
143 143 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
144 144
145 145 var sizeExceeded = false;
146 146 $.each(files, function() {
147 147 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
148 148 });
149 149 if (sizeExceeded) {
150 150 window.alert(maxFileSizeExceeded);
151 151 } else {
152 152 $.each(files, function() {addFile(inputEl, this, true);});
153 153 }
154 154 }
155 155
156 156 function handleFileDropEvent(e) {
157 157
158 158 $(this).removeClass('fileover');
159 159 blockEventPropagation(e);
160 160
161 161 if ($.inArray('Files', e.dataTransfer.types) > -1) {
162 162 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
163 163 }
164 164 }
165 165
166 166 function dragOverHandler(e) {
167 167 $(this).addClass('fileover');
168 168 blockEventPropagation(e);
169 169 }
170 170
171 171 function dragOutHandler(e) {
172 172 $(this).removeClass('fileover');
173 173 blockEventPropagation(e);
174 174 }
175 175
176 176 function setupFileDrop() {
177 177 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
178 178
179 179 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
180 180
181 181 $('form div.box').has('input:file').each(function() {
182 182 $(this).on({
183 183 dragover: dragOverHandler,
184 184 dragleave: dragOutHandler,
185 185 drop: handleFileDropEvent
186 186 });
187 187 });
188 188 }
189 189 }
190 190
191 191 $(document).ready(setupFileDrop);
192 $(document).ready(function(){
193 $("input.deleted_attachment").change(function(){
194 $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
195 }).change();
196 });
@@ -1,1389 +1,1389
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 5 #content h1, h2, h3, h4 {color: #555;}
6 6 h2, .wiki h1 {font-size: 20px;}
7 7 h3, .wiki h2 {font-size: 16px;}
8 8 h4, .wiki h3 {font-size: 13px;}
9 9 h4 {border-bottom: 1px dotted #bbb;}
10 10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
11 11
12 12 /***** Layout *****/
13 13 #wrapper {background: white;overflow: hidden;}
14 14
15 15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
16 16 #top-menu ul {margin: 0; padding: 0;}
17 17 #top-menu li {
18 18 float:left;
19 19 list-style-type:none;
20 20 margin: 0px 0px 0px 0px;
21 21 padding: 0px 0px 0px 0px;
22 22 white-space:nowrap;
23 23 }
24 24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
25 25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
26 26
27 27 #account {float:right;}
28 28
29 29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
30 30 #header a {color:#f8f8f8;}
31 31 #header h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
32 32 #header h1 .breadcrumbs { display:block; font-size: .6em; font-weight: normal; }
33 33 #quick-search {float:right;}
34 34
35 35 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
36 36 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
37 37 #main-menu li {
38 38 float:none;
39 39 list-style-type:none;
40 40 margin: 0px 2px 0px 0px;
41 41 padding: 0px 0px 0px 0px;
42 42 white-space:nowrap;
43 43 display:inline-block;
44 44 }
45 45 #main-menu li a {
46 46 display: block;
47 47 color: #fff;
48 48 text-decoration: none;
49 49 font-weight: bold;
50 50 margin: 0;
51 51 padding: 4px 10px 4px 10px;
52 52 }
53 53 #main-menu li a:hover {background:#759FCF; color:#fff;}
54 54 #main-menu li:hover ul.menu-children, #main-menu li ul.menu-children.visible {display: block;}
55 55 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
56 56 #main-menu li a.new-object { background-color:#759FCF; }
57 57
58 58 #main-menu .menu-children {
59 59 display: none;
60 60 position:absolute;
61 61 width: inherit;
62 62 z-index:1;
63 63 background-color:#fff;
64 64 border-right: 1px solid #759FCF;
65 65 border-bottom: 1px solid #759FCF;
66 66 border-left: 1px solid #759FCF;
67 67 }
68 68 #main-menu .menu-children li {float:left; clear:both; width:100%;}
69 69 #main-menu .menu-children li a {color: #555; background-color:#fff; font-weight:normal;}
70 70 #main-menu .menu-children li a:hover {color: #fff; background-color: #759FCF;}
71 71
72 72 #main-menu .tabs-buttons {
73 73 right: 6px;
74 74 background-color: transparent;
75 75 border-bottom-color: transparent;
76 76 }
77 77
78 78 #admin-menu ul {margin: 0; padding: 0;}
79 79 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
80 80
81 81 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
82 82 #admin-menu a.projects { background-image: url(../images/projects.png); }
83 83 #admin-menu a.users { background-image: url(../images/user.png); }
84 84 #admin-menu a.groups { background-image: url(../images/group.png); }
85 85 #admin-menu a.roles { background-image: url(../images/database_key.png); }
86 86 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
87 87 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
88 88 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
89 89 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
90 90 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
91 91 #admin-menu a.settings { background-image: url(../images/changeset.png); }
92 92 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
93 93 #admin-menu a.info { background-image: url(../images/help.png); }
94 94 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
95 95
96 96 #main {background-color:#EEEEEE;}
97 97
98 98 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
99 99 * html #sidebar{ width: 22%; }
100 100 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
101 101 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
102 102 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
103 103 #sidebar .contextual { margin-right: 1em; }
104 104 #sidebar ul, ul.flat {margin: 0; padding: 0;}
105 105 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
106 106 #sidebar div.wiki ul {margin:inherit; padding-left:40px;}
107 107 #sidebar div.wiki ul li {list-style-type:inherit;}
108 108
109 109 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
110 110 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
111 111 html>body #content { min-height: 600px; }
112 112 * html body #content { height: 600px; } /* IE */
113 113
114 114 #main.nosidebar #sidebar{ display: none; }
115 115 #main.nosidebar #content{ width: auto; border-right: 0; }
116 116
117 117 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
118 118
119 119 #login-form {margin:5em auto 2em auto; padding:20px; width:340px; border:1px solid #FDBF3B; background-color:#FFEBC1; border-radius:4px; box-sizing: border-box;}
120 120 #login-form label {display:block; margin-bottom:5px;}
121 121 #login-form input[type=text], #login-form input[type=password] {border:1px solid #ccc; border-radius:3px; margin-bottom:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
122 122 #login-form label {font-weight:bold;}
123 123 #login-form label[for=autologin] {font-weight:normal;}
124 124 #login-form a.lost_password {float:right; font-weight:normal;}
125 125 #login-form input#openid_url {background:#fff url(../images/openid-bg.gif) no-repeat 4px 50%; padding-left:24px !important;}
126 126 #login-form input#login-submit {margin-top:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
127 127
128 128 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
129 129 div.modal h3.title {display:none;}
130 130 div.modal p.buttons {text-align:right; margin-bottom:0;}
131 131 div.modal .box p {margin: 0.3em 0;}
132 132
133 133 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
134 134
135 135 .mobile-show {display: none;}
136 136
137 137 /***** Links *****/
138 138 a, a:link, a:visited{ color: #169; text-decoration: none; }
139 139 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
140 140 a img{ border: 0; }
141 141
142 142 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
143 143 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
144 144 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
145 145
146 146 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
147 147 #sidebar a.selected:hover {text-decoration:none;}
148 148 #admin-menu a {line-height:1.7em;}
149 149 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
150 150
151 151 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
152 152 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
153 153
154 154 a#toggle-completed-versions {color:#999;}
155 155
156 156 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
157 157
158 158 /***** Tables *****/
159 159 table.list, .table-list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
160 160 table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
161 161 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
162 162 table.list td.id { width: 2%; text-align: center;}
163 163 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
164 164 table.list td.tick {width:15%}
165 165 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
166 166 table.list td.checkbox input {padding:0px;}
167 167 table.list td.buttons, div.buttons { white-space:nowrap; text-align: right; }
168 168 table.list td.buttons a, div.buttons a { margin-right: 0.6em; }
169 169 table.list td.buttons img, div.buttons img {vertical-align:middle;}
170 170 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
171 171 table.list table.progress td {padding-right:0px;}
172 172 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
173 173 #role-permissions-trackers table.list th {white-space:normal;}
174 174
175 175 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
176 176
177 177 tr.project td.name a { white-space:nowrap; }
178 178 tr.project.closed, tr.project.archived { color: #aaa; }
179 179 tr.project.closed a, tr.project.archived a { color: #aaa; }
180 180
181 181 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
182 182 tr.project.idnt-1 td.name {padding-left: 0.5em;}
183 183 tr.project.idnt-2 td.name {padding-left: 2em;}
184 184 tr.project.idnt-3 td.name {padding-left: 3.5em;}
185 185 tr.project.idnt-4 td.name {padding-left: 5em;}
186 186 tr.project.idnt-5 td.name {padding-left: 6.5em;}
187 187 tr.project.idnt-6 td.name {padding-left: 8em;}
188 188 tr.project.idnt-7 td.name {padding-left: 9.5em;}
189 189 tr.project.idnt-8 td.name {padding-left: 11em;}
190 190 tr.project.idnt-9 td.name {padding-left: 12.5em;}
191 191
192 192 tr.issue { text-align: center; white-space: nowrap; }
193 193 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
194 194 tr.issue td.relations { text-align: left; }
195 195 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
196 196 tr.issue td.relations span {white-space: nowrap;}
197 197 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
198 198 table.issues td.description pre {white-space:normal;}
199 199
200 200 tr.issue.idnt td.subject {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%;}
201 201 tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
202 202 tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
203 203 tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
204 204 tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
205 205 tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
206 206 tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
207 207 tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
208 208 tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
209 209 tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
210 210
211 211 table.issue-report {table-layout:fixed;}
212 212
213 213 tr.entry { border: 1px solid #f8f8f8; }
214 214 tr.entry td { white-space: nowrap; }
215 215 tr.entry td.filename {width:30%; text-align:left;}
216 216 tr.entry td.filename_no_report {width:70%; text-align:left;}
217 217 tr.entry td.size { text-align: right; font-size: 90%; }
218 218 tr.entry td.revision, tr.entry td.author { text-align: center; }
219 219 tr.entry td.age { text-align: right; }
220 220 tr.entry.file td.filename a { margin-left: 16px; }
221 221 tr.entry.file td.filename_no_report a { margin-left: 16px; }
222 222
223 223 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
224 224 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
225 225
226 226 tr.changeset { height: 20px }
227 227 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
228 228 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
229 229 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
230 230 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
231 231
232 232 table.files tbody th {text-align:left;}
233 233 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
234 234 table.files tr.file td.digest { font-size: 80%; }
235 235
236 236 table.members td.roles, table.memberships td.roles { width: 45%; }
237 237
238 238 tr.message { height: 2.6em; }
239 239 tr.message td.subject { padding-left: 20px; }
240 240 tr.message td.created_on { white-space: nowrap; }
241 241 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
242 242 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
243 243 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
244 244
245 245 tr.version.closed, tr.version.closed a { color: #999; }
246 246 tr.version td.name { padding-left: 20px; }
247 247 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
248 248 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
249 249
250 250 tr.user td {width:13%;white-space: nowrap;}
251 251 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
252 252 tr.user td.email { width:18%; }
253 253 tr.user.locked, tr.user.registered { color: #aaa; }
254 254 tr.user.locked a, tr.user.registered a { color: #aaa; }
255 255
256 256 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
257 257
258 258 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
259 259
260 260 tr.time-entry { text-align: center; white-space: nowrap; }
261 261 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
262 262 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
263 263 td.hours .hours-dec { font-size: 0.9em; }
264 264
265 265 table.plugins td { vertical-align: middle; }
266 266 table.plugins td.configure { text-align: right; padding-right: 1em; }
267 267 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
268 268 table.plugins span.description { display: block; font-size: 0.9em; }
269 269 table.plugins span.url { display: block; font-size: 0.9em; }
270 270
271 271 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
272 272 tr.group span.name {font-weight:bold;}
273 273 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
274 274 tr.group span.totals {color: #aaa; font-size: 80%;}
275 275 tr.group span.totals .value {font-weight:bold; color:#777;}
276 276 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
277 277 tr.group:hover a.toggle-all { display:inline;}
278 278 a.toggle-all:hover {text-decoration:none;}
279 279
280 280 table.list tbody tr:hover { background-color:#ffffdd; }
281 281 table.list tbody tr.group:hover { background-color:inherit; }
282 282 table td {padding:2px;}
283 283 table p {margin:0;}
284 284 .odd {background-color:#f6f7f8;}
285 285 .even {background-color: #fff;}
286 286
287 287 tr.builtin td.name {font-style:italic;}
288 288
289 289 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
290 290 a.sort.asc { background-image: url(../images/sort_asc.png); }
291 291 a.sort.desc { background-image: url(../images/sort_desc.png); }
292 292
293 293 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
294 294 table.boards td.last-message {text-align:left;font-size:80%;}
295 295
296 296 div.table-list.boards .table-list-cell.name {width: 30%;}
297 297
298 298 table.messages td.last_message {text-align:left;}
299 299
300 300 #query_form_content {font-size:90%;}
301 301
302 302 .query_sort_criteria_count {
303 303 display: inline-block;
304 304 min-width: 1em;
305 305 }
306 306
307 307 table.query-columns {
308 308 border-collapse: collapse;
309 309 border: 0;
310 310 }
311 311
312 312 table.query-columns td.buttons {
313 313 vertical-align: middle;
314 314 text-align: center;
315 315 }
316 316 table.query-columns td.buttons input[type=button] {width:35px;}
317 317 .query-totals {text-align:right;}
318 318 .query-totals>span {margin-left:0.6em;}
319 319 .query-totals .value {font-weight:bold;}
320 320 body.controller-issues .query-totals {margin-top:-2.3em;}
321 321
322 322 td.center {text-align:center;}
323 323
324 324 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
325 325
326 326 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
327 327 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
328 328 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
329 329 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
330 330 div.spent_time h3 { background: url(../images/time.png) no-repeat 0% 50%; padding-left: 20px; }
331 331
332 332 #watchers select {width: 95%; display: block;}
333 333 #watchers a.delete {opacity: 0.4; margin-left: 5px;}
334 334 #watchers a.delete:hover {opacity: 1;}
335 335 #watchers img.gravatar {margin: 0 4px 2px 0;}
336 336
337 337 span#watchers_inputs {overflow:auto; display:block;}
338 338 span.search_for_watchers {display:block;}
339 339 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
340 340 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
341 341
342 342
343 343 .highlight { background-color: #FCFD8D;}
344 344 .highlight.token-1 { background-color: #faa;}
345 345 .highlight.token-2 { background-color: #afa;}
346 346 .highlight.token-3 { background-color: #aaf;}
347 347
348 348 .box{
349 349 padding:6px;
350 350 margin-bottom: 10px;
351 351 background-color:#f6f6f6;
352 352 color:#505050;
353 353 line-height:1.5em;
354 354 border: 1px solid #e4e4e4;
355 355 word-wrap: break-word;
356 356 border-radius: 3px;
357 357 }
358 358
359 359 div.square {
360 360 border: 1px solid #999;
361 361 float: left;
362 362 margin: .3em .4em 0 .4em;
363 363 overflow: hidden;
364 364 width: .6em; height: .6em;
365 365 }
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
367 367 .contextual input, .contextual select {font-size:0.9em;}
368 368 .message .contextual { margin-top: 0; }
369 369
370 370 .splitcontent {overflow:auto;}
371 371 .splitcontentleft{float:left; width:49%;}
372 372 .splitcontentright{float:right; width:49%;}
373 373 form {display: inline;}
374 374 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
375 375 fieldset {border: 1px solid #e4e4e4; margin:0;}
376 376 legend {color: #333;}
377 377 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
378 378 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
379 379 blockquote blockquote { margin-left: 0;}
380 380 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
381 381 textarea.wiki-edit {width:99%; resize:vertical;}
382 382 li p {margin-top: 0;}
383 383 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
384 384 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
385 385 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
386 386 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
387 387 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
388 388 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
389 389
390 390 div.issue div.subject div div { padding-left: 16px; }
391 391 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
392 392 div.issue div.subject>div>p { margin-top: 0.5em; }
393 393 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
394 394 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
395 395 div.issue .next-prev-links {color:#999;}
396 396 div.issue .attributes {margin-top: 2em;}
397 397 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
398 398 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
399 399 div.issue.overdue .due-date .value { color: #c22; }
400 400
401 401 #issue_tree table.issues, #relations table.issues { border: 0; }
402 402 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
403 403 #relations td.buttons {padding:0;}
404 404
405 405 fieldset.collapsible {border-width: 1px 0 0 0;}
406 406 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
407 407 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
408 408
409 409 fieldset#date-range p { margin: 2px 0 2px 0; }
410 410 fieldset#filters table { border-collapse: collapse; }
411 411 fieldset#filters table td { padding: 0; vertical-align: middle; }
412 412 fieldset#filters tr.filter { height: 2.1em; }
413 413 fieldset#filters td.field { width:230px; }
414 414 fieldset#filters td.operator { width:130px; }
415 415 fieldset#filters td.operator select {max-width:120px;}
416 416 fieldset#filters td.values { white-space:nowrap; }
417 417 fieldset#filters td.values select {min-width:130px; max-width:200px;}
418 418 fieldset#filters td.values input {height:1em;}
419 419
420 420 #filters-table {width:60%; float:left;}
421 421 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
422 422
423 423 #issue_is_private_wrap {float:right; margin-right:1em;}
424 424 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:16px; margin-left:0; margin-right:5px; cursor:pointer;}
425 425 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
426 426
427 427 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
428 428 div#issue-changesets div.changeset { padding: 4px;}
429 429 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
430 430 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
431 431
432 432 div.journal {overflow:auto;}
433 433 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
434 434 div.journal ul.details {color:#959595; margin-bottom: 1.5em;}
435 435 div.journal ul.details a {color:#70A7CD;}
436 436 div.journal ul.details a:hover {color:#D14848;}
437 437
438 438 div#activity dl, #search-results { margin-left: 2em; }
439 439 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
440 440 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
441 441 div#activity dt.me .time { border-bottom: 1px solid #999; }
442 442 div#activity dt .time { color: #777; font-size: 80%; }
443 443 div#activity dd .description, #search-results dd .description { font-style: italic; }
444 444 div#activity span.project:after, #search-results span.project:after { content: " -"; }
445 445 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
446 446 div#activity dt.grouped {margin-left:5em;}
447 447 div#activity dd.grouped {margin-left:9em;}
448 448
449 449 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
450 450
451 451 div#search-results-counts {float:right;}
452 452 div#search-results-counts ul { margin-top: 0.5em; }
453 453 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
454 454
455 455 dt.issue { background-image: url(../images/ticket.png); }
456 456 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
457 457 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
458 458 dt.issue-note { background-image: url(../images/ticket_note.png); }
459 459 dt.changeset { background-image: url(../images/changeset.png); }
460 460 dt.news { background-image: url(../images/news.png); }
461 461 dt.message { background-image: url(../images/message.png); }
462 462 dt.reply { background-image: url(../images/comments.png); }
463 463 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
464 464 dt.attachment { background-image: url(../images/attachment.png); }
465 465 dt.document { background-image: url(../images/document.png); }
466 466 dt.project { background-image: url(../images/projects.png); }
467 467 dt.time-entry { background-image: url(../images/time.png); }
468 468
469 469 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
470 470
471 471 div#roadmap .related-issues { margin-bottom: 1em; }
472 472 div#roadmap .related-issues td.checkbox { display: none; }
473 473 div#roadmap .wiki h1:first-child { display: none; }
474 474 div#roadmap .wiki h1 { font-size: 120%; }
475 475 div#roadmap .wiki h2 { font-size: 110%; }
476 476 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
477 477
478 478 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
479 479 div#version-summary fieldset { margin-bottom: 1em; }
480 480 div#version-summary fieldset.time-tracking table { width:100%; }
481 481 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
482 482
483 483 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
484 484 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
485 485 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
486 486 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
487 487 table#time-report .hours-dec { font-size: 0.9em; }
488 488
489 489 div.wiki-page .contextual a {opacity: 0.4}
490 490 div.wiki-page .contextual a:hover {opacity: 1}
491 491
492 492 form .attributes select { width: 60%; }
493 493 form .attributes select + a.icon-only { vertical-align: middle; margin-left: 4px; }
494 494 input#issue_subject, input#document_title { width: 99%; }
495 495 select#issue_done_ratio { width: 95px; }
496 496
497 497 ul.projects {margin:0; padding-left:1em;}
498 498 ul.projects ul {padding-left:1.6em;}
499 499 ul.projects.root {margin:0; padding:0;}
500 500 ul.projects li {list-style-type:none;}
501 501
502 502 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
503 503 #projects-index ul.projects li.root {margin-bottom: 1em;}
504 504 #projects-index ul.projects li.child {margin-top: 1em;}
505 505 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
506 506 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
507 507
508 508 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
509 509
510 510 #related-issues li img {vertical-align:middle;}
511 511
512 512 ul.properties {padding:0; font-size: 0.9em; color: #777;}
513 513 ul.properties li {list-style-type:none;}
514 514 ul.properties li span {font-style:italic;}
515 515
516 516 .total-hours { font-size: 110%; font-weight: bold; }
517 517 .total-hours span.hours-int { font-size: 120%; }
518 518
519 519 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em; position: relative;}
520 520 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
521 521
522 522 #workflow_copy_form select { width: 200px; }
523 523 table.transitions td.enabled {background: #bfb;}
524 524 #workflow_form table select {font-size:90%; max-width:100px;}
525 525 table.fields_permissions td.readonly {background:#ddd;}
526 526 table.fields_permissions td.required {background:#d88;}
527 527
528 528 select.expandable {vertical-align:top;}
529 529
530 530 textarea#custom_field_possible_values {width: 95%; resize:vertical}
531 531 textarea#custom_field_default_value {width: 95%; resize:vertical}
532 532 .sort-handle {display:inline-block; vertical-align:middle;}
533 533
534 534 input#content_comments {width: 99%}
535 535
536 536 span.pagination {margin-left:3px; color:#888; display:block;}
537 537 .pagination ul.pages {
538 538 margin: 0 5px 0 0;
539 539 padding: 0;
540 540 display: inline;
541 541 }
542 542 .pagination ul.pages li {
543 543 display: inline-block;
544 544 padding: 0;
545 545 border: 1px solid #ddd;
546 546 margin-left: -1px;
547 547 line-height: 2em;
548 548 margin-bottom: 1em;
549 549 white-space: nowrap;
550 550 text-align: center;
551 551 }
552 552 .pagination ul.pages li a,
553 553 .pagination ul.pages li span {
554 554 padding: 3px 8px;
555 555 }
556 556 .pagination ul.pages li:first-child {
557 557 border-top-left-radius: 4px;
558 558 border-bottom-left-radius: 4px;
559 559 }
560 560 .pagination ul.pages li:last-child {
561 561 border-top-right-radius: 4px;
562 562 border-bottom-right-radius: 4px;
563 563 }
564 564 .pagination ul.pages li.current {
565 565 color: white;
566 566 background-color: #628DB6;
567 567 border-color: #628DB6;
568 568 }
569 569 .pagination ul.pages li.page:hover {
570 570 background-color: #ddd;
571 571 }
572 572 .pagination ul.pages li.page a:hover,
573 573 .pagination ul.pages li.page a:active {
574 574 color: #169;
575 575 text-decoration: inherit;
576 576 }
577 577 .pagination .per-page span.selected {
578 578 font-weight: bold;
579 579 }
580 580 span.pagination>span {white-space:nowrap;}
581 581
582 582 #search-form fieldset p {margin:0.2em 0;}
583 583
584 584 /***** Tabular forms ******/
585 585 .tabular p{
586 586 margin: 0;
587 587 padding: 3px 0 3px 0;
588 588 padding-left: 180px; /* width of left column containing the label elements */
589 589 min-height: 1.8em;
590 590 clear:left;
591 591 }
592 592
593 593 html>body .tabular p {overflow:hidden;}
594 594
595 595 .tabular input, .tabular select {max-width:95%}
596 596 .tabular textarea {width:95%; resize:vertical;}
597 597
598 598 .tabular label{
599 599 font-weight: bold;
600 600 float: left;
601 601 text-align: right;
602 602 /* width of left column */
603 603 margin-left: -180px;
604 604 /* width of labels. Should be smaller than left column to create some right margin */
605 605 width: 175px;
606 606 }
607 607
608 608 .tabular label.floating{
609 609 font-weight: normal;
610 610 margin-left: 0px;
611 611 text-align: left;
612 612 width: 270px;
613 613 }
614 614
615 615 .tabular label.block{
616 616 font-weight: normal;
617 617 margin-left: 0px !important;
618 618 text-align: left;
619 619 float: none;
620 620 display: block;
621 621 width: auto !important;
622 622 }
623 623
624 624 .tabular label.inline{
625 625 font-weight: normal;
626 626 float:none;
627 627 margin-left: 5px !important;
628 628 width: auto;
629 629 }
630 630
631 631 label.no-css {
632 632 font-weight: inherit;
633 633 float:none;
634 634 text-align:left;
635 635 margin-left:0px;
636 636 width:auto;
637 637 }
638 638 input#time_entry_comments { width: 90%;}
639 639
640 640 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
641 641
642 642 .tabular.settings p{ padding-left: 300px; }
643 643 .tabular.settings label{ margin-left: -300px; width: 295px; }
644 644 .tabular.settings textarea { width: 99%; }
645 645
646 646 .settings.enabled_scm table {width:100%}
647 647 .settings.enabled_scm td.scm_name{ font-weight: bold; }
648 648
649 649 fieldset.settings label { display: block; }
650 650 fieldset#notified_events .parent { padding-left: 20px; }
651 651
652 652 span.required {color: #bb0000;}
653 653 .summary {font-style: italic;}
654 654
655 655 .check_box_group {
656 656 display:block;
657 657 width:95%;
658 658 max-height:300px;
659 659 overflow-y:auto;
660 660 padding:2px 4px 4px 2px;
661 661 background:#fff;
662 662 border:1px solid #9EB1C2;
663 663 border-radius:2px
664 664 }
665 665 .check_box_group label {
666 666 font-weight: normal;
667 667 margin-left: 0px !important;
668 668 text-align: left;
669 669 float: none;
670 670 display: block;
671 671 width: auto;
672 672 }
673 673 .check_box_group.bool_cf {border:0; background:inherit;}
674 674 .check_box_group.bool_cf label {display: inline;}
675 675
676 #attachments_fields input.description {margin-left:4px; width:340px;}
677 #attachments_fields span {display:block; white-space:nowrap;}
678 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
676 #attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
677 #attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
678 #attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
679 #attachments_fields input.filename {height:1.8em;}
679 680 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
680 681 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
681 682 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
682 683 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
683 684 a.remove-upload:hover {text-decoration:none !important;}
685 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
684 686
685 687 div.fileover { background-color: lavender; }
686 688
687 689 div.attachments { margin: 12px 0; }
688 690 div.attachments p { margin:4px 0 2px 0; }
689 691 div.attachments img { vertical-align: middle; }
690 692 div.attachments span.author { font-size: 0.9em; color: #888; }
691 693
692 694 div.thumbnails {margin:0.6em;}
693 695 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
694 696 div.thumbnails img {margin: 3px; vertical-align: middle;}
695 697 #history div.thumbnails {margin-left: 2em;}
696 698
697 699 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
698 700 .other-formats span + span:before { content: "| "; }
699 701
700 702 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
701 703
702 704 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
703 705 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
704 706
705 707 textarea.text_cf {width:95%; resize:vertical;}
706 708 input.string_cf, input.link_cf {width:95%;}
707 709 select.bool_cf {width:auto !important;}
708 710
709 711 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
710 712
711 713 #tab-content-users .splitcontentleft {width: 64%;}
712 714 #tab-content-users .splitcontentright {width: 34%;}
713 715 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
714 716 #tab-content-users fieldset legend {font-weight: bold;}
715 717 #tab-content-users fieldset label {display: block;}
716 718 #tab-content-users #principals {max-height: 400px; overflow: auto;}
717 719
718 720 #users_for_watcher {height: 200px; overflow:auto;}
719 721 #users_for_watcher label {display: block;}
720 722
721 723 table.members td.name {padding-left: 20px;}
722 724 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
723 725
724 726 input#principal_search, input#user_search {width:90%}
725 727 .roles-selection label {display:inline-block; width:210px;}
726 728
727 729 input.autocomplete {
728 730 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
729 731 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
730 732 }
731 733 input.autocomplete.ajax-loading {
732 734 background-image: url(../images/loading.gif);
733 735 }
734 736
735 737 .role-visibility {padding-left:2em;}
736 738
737 739 .objects-selection {
738 740 height: 300px;
739 741 overflow: auto;
740 742 margin-bottom: 1em;
741 743 }
742 744
743 745 .objects-selection label {
744 746 display: block;
745 747 }
746 748
747 749 .objects-selection>div {
748 750 column-count: auto;
749 751 column-width: 200px;
750 752 -webkit-column-count: auto;
751 753 -webkit-column-width: 200px;
752 754 -webkit-column-gap : 0.5rem;
753 755 -webkit-column-rule: 1px solid #ccc;
754 756 -moz-column-count: auto;
755 757 -moz-column-width: 200px;
756 758 -moz-column-gap : 0.5rem;
757 759 -moz-column-rule: 1px solid #ccc;
758 760 }
759 761
760 762 /***** Flash & error messages ****/
761 763 #errorExplanation, div.flash, .nodata, .warning, .conflict {
762 764 padding: 6px 4px 6px 30px;
763 765 margin-bottom: 12px;
764 766 font-size: 1.1em;
765 767 border: 1px solid;
766 768 border-radius: 3px;
767 769 }
768 770
769 771 div.flash {margin-top: 8px;}
770 772
771 773 div.flash.error, #errorExplanation {
772 774 background: url(../images/exclamation.png) 8px 50% no-repeat;
773 775 background-color: #ffe3e3;
774 776 border-color: #d88;
775 777 color: #880000;
776 778 }
777 779
778 780 div.flash.notice {
779 781 background: url(../images/true.png) 8px 5px no-repeat;
780 782 background-color: #dfffdf;
781 783 border-color: #9fcf9f;
782 784 color: #005f00;
783 785 }
784 786
785 787 div.flash.warning, .conflict {
786 788 background: url(../images/warning.png) 8px 5px no-repeat;
787 789 background-color: #F3EDD1;
788 790 border-color: #eadbbc;
789 791 color: #A6750C;
790 792 text-align: left;
791 793 }
792 794
793 795 .nodata, .warning {
794 796 text-align: center;
795 797 background-color: #F3EDD1;
796 798 border-color: #eadbbc;
797 799 color: #A6750C;
798 800 }
799 801
800 802 #errorExplanation ul { font-size: 0.9em;}
801 803 #errorExplanation h2, #errorExplanation p { display: none; }
802 804
803 805 .conflict-details {font-size:80%;}
804 806
805 807 /***** Ajax indicator ******/
806 808 #ajax-indicator {
807 809 position: absolute; /* fixed not supported by IE */
808 810 background-color:#eee;
809 811 border: 1px solid #bbb;
810 812 top:35%;
811 813 left:40%;
812 814 width:20%;
813 815 font-weight:bold;
814 816 text-align:center;
815 817 padding:0.6em;
816 818 z-index:100;
817 819 opacity: 0.5;
818 820 }
819 821
820 822 html>body #ajax-indicator { position: fixed; }
821 823
822 824 #ajax-indicator span {
823 825 background-position: 0% 40%;
824 826 background-repeat: no-repeat;
825 827 background-image: url(../images/loading.gif);
826 828 padding-left: 26px;
827 829 vertical-align: bottom;
828 830 }
829 831
830 832 /***** Calendar *****/
831 833 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
832 834 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
833 835 table.cal thead th.week-number {width: auto;}
834 836 table.cal tbody tr {height: 100px;}
835 837 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
836 838 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
837 839 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
838 840 table.cal td.odd p.day-num {color: #bbb;}
839 841 table.cal td.today {background:#ffffdd;}
840 842 table.cal td.today p.day-num {font-weight: bold;}
841 843 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
842 844 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
843 845 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
844 846 p.cal.legend span {display:block;}
845 847
846 848 /***** Tooltips ******/
847 849 .tooltip{position:relative;z-index:24;}
848 850 .tooltip:hover{z-index:25;color:#000;}
849 851 .tooltip span.tip{display: none; text-align:left;}
850 852
851 853 div.tooltip:hover span.tip{
852 854 display:block;
853 855 position:absolute;
854 856 top:12px; width:270px;
855 857 border:1px solid #555;
856 858 background-color:#fff;
857 859 padding: 4px;
858 860 font-size: 0.8em;
859 861 color:#505050;
860 862 }
861 863
862 864 img.ui-datepicker-trigger {
863 865 cursor: pointer;
864 866 vertical-align: middle;
865 867 margin-left: 4px;
866 868 }
867 869
868 870 /***** Progress bar *****/
869 871 table.progress {
870 872 border-collapse: collapse;
871 873 border-spacing: 0pt;
872 874 empty-cells: show;
873 875 text-align: center;
874 876 float:left;
875 877 margin: 1px 6px 1px 0px;
876 878 }
877 879
878 880 table.progress {width:80px;}
879 881 table.progress td { height: 1em; }
880 882 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
881 883 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
882 884 table.progress td.todo { background: #eee none repeat scroll 0%; }
883 885 p.percent {font-size: 80%; margin:0;}
884 886 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
885 887
886 888 .version-overview table.progress {width:40em;}
887 889 .version-overview table.progress td { height: 1.2em; }
888 890
889 891 /***** Tabs *****/
890 892 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
891 893 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
892 894 #content .tabs ul li {
893 895 float:left;
894 896 list-style-type:none;
895 897 white-space:nowrap;
896 898 margin-right:4px;
897 899 background:#fff;
898 900 position:relative;
899 901 margin-bottom:-1px;
900 902 }
901 903 #content .tabs ul li a{
902 904 display:block;
903 905 font-size: 0.9em;
904 906 text-decoration:none;
905 907 line-height:1.3em;
906 908 padding:4px 6px 4px 6px;
907 909 border: 1px solid #ccc;
908 910 border-bottom: 1px solid #bbbbbb;
909 911 background-color: #f6f6f6;
910 912 color:#999;
911 913 font-weight:bold;
912 914 border-top-left-radius:3px;
913 915 border-top-right-radius:3px;
914 916 }
915 917
916 918 #content .tabs ul li a:hover {
917 919 background-color: #ffffdd;
918 920 text-decoration:none;
919 921 }
920 922
921 923 #content .tabs ul li a.selected {
922 924 background-color: #fff;
923 925 border: 1px solid #bbbbbb;
924 926 border-bottom: 1px solid #fff;
925 927 color:#444;
926 928 }
927 929
928 930 #content .tabs ul li a.selected:hover {background-color: #fff;}
929 931
930 932 div.tabs-buttons { position:absolute; right: 0; width: 54px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
931 933
932 934 button.tab-left, button.tab-right {
933 935 font-size: 0.9em;
934 936 cursor: pointer;
935 937 height:24px;
936 938 border: 1px solid #ccc;
937 939 border-bottom: 1px solid #bbbbbb;
938 940 position:absolute;
939 941 padding:4px;
940 942 width: 20px;
941 943 bottom: -1px;
942 944 }
943 945 button.tab-left:hover, button.tab-right:hover {
944 946 background-color: #f5f5f5;
945 947 }
946 948 button.tab-left:focus, button.tab-right:focus {
947 949 outline: 0;
948 950 }
949 951
950 952 button.tab-left {
951 953 right: 20px;
952 954 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
953 955 border-top-left-radius:3px;
954 956 }
955 957
956 958 button.tab-right {
957 959 right: 0;
958 960 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
959 961 border-top-right-radius:3px;
960 962 }
961 963
962 964 button.tab-left.disabled, button.tab-right.disabled {
963 965 background-color: #ccc;
964 966 cursor: unset;
965 967 }
966 968
967 969 /***** Diff *****/
968 970 .diff_out { background: #fcc; }
969 971 .diff_out span { background: #faa; }
970 972 .diff_in { background: #cfc; }
971 973 .diff_in span { background: #afa; }
972 974
973 975 .text-diff {
974 976 padding: 1em;
975 977 background-color:#f6f6f6;
976 978 color:#505050;
977 979 border: 1px solid #e4e4e4;
978 980 }
979 981
980 982 /***** Wiki *****/
981 983 div.wiki table {
982 984 border-collapse: collapse;
983 985 margin-bottom: 1em;
984 986 }
985 987
986 988 div.wiki table, div.wiki td, div.wiki th {
987 989 border: 1px solid #bbb;
988 990 padding: 4px;
989 991 }
990 992
991 993 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
992 994
993 995 div.wiki .external {
994 996 background-position: 0% 60%;
995 997 background-repeat: no-repeat;
996 998 padding-left: 12px;
997 999 background-image: url(../images/external.png);
998 1000 }
999 1001
1000 1002 div.wiki a {word-wrap: break-word;}
1001 1003 div.wiki a.new {color: #b73535;}
1002 1004
1003 1005 div.wiki ul, div.wiki ol {margin-bottom:1em;}
1004 1006 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
1005 1007
1006 1008 div.wiki pre {
1007 1009 margin: 1em 1em 1em 1.6em;
1008 1010 padding: 8px;
1009 1011 background-color: #fafafa;
1010 1012 border: 1px solid #e2e2e2;
1011 1013 border-radius: 3px;
1012 1014 width:auto;
1013 1015 overflow-x: auto;
1014 1016 overflow-y: hidden;
1015 1017 }
1016 1018
1017 1019 div.wiki ul.toc {
1018 1020 background-color: #ffffdd;
1019 1021 border: 1px solid #e4e4e4;
1020 1022 padding: 4px;
1021 1023 line-height: 1.2em;
1022 1024 margin-bottom: 12px;
1023 1025 margin-right: 12px;
1024 1026 margin-left: 0;
1025 1027 display: table
1026 1028 }
1027 1029 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
1028 1030
1029 1031 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
1030 1032 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
1031 1033 div.wiki ul.toc ul { margin: 0; padding: 0; }
1032 1034 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
1033 1035 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
1034 1036 div.wiki ul.toc a {
1035 1037 font-size: 0.9em;
1036 1038 font-weight: normal;
1037 1039 text-decoration: none;
1038 1040 color: #606060;
1039 1041 }
1040 1042 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
1041 1043
1042 1044 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
1043 1045 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1044 1046 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1045 1047
1046 1048 div.wiki img {vertical-align:middle; max-width:100%;}
1047 1049
1048 1050 /***** My page layout *****/
1049 1051 .block-receiver {
1050 1052 border:1px dashed #c0c0c0;
1051 1053 margin-bottom: 20px;
1052 1054 padding: 15px 0 15px 0;
1053 1055 }
1054 1056
1055 1057 .mypage-box {
1056 1058 margin:0 0 20px 0;
1057 1059 color:#505050;
1058 1060 line-height:1.5em;
1059 1061 }
1060 1062
1061 1063 .handle {cursor: move;}
1062 1064
1063 1065 a.close-icon {
1064 1066 display:block;
1065 1067 margin-top:3px;
1066 1068 overflow:hidden;
1067 1069 width:12px;
1068 1070 height:12px;
1069 1071 background-repeat: no-repeat;
1070 1072 cursor:pointer;
1071 1073 background-image:url('../images/close.png');
1072 1074 }
1073 1075 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1074 1076
1075 1077 /***** Gantt chart *****/
1076 1078 .gantt_hdr {
1077 1079 position:absolute;
1078 1080 top:0;
1079 1081 height:16px;
1080 1082 border-top: 1px solid #c0c0c0;
1081 1083 border-bottom: 1px solid #c0c0c0;
1082 1084 border-right: 1px solid #c0c0c0;
1083 1085 text-align: center;
1084 1086 overflow: hidden;
1085 1087 }
1086 1088
1087 1089 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1088 1090
1089 1091 .gantt_subjects { font-size: 0.8em; }
1090 1092 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1091 1093
1092 1094 .task {
1093 1095 position: absolute;
1094 1096 height:8px;
1095 1097 font-size:0.8em;
1096 1098 color:#888;
1097 1099 padding:0;
1098 1100 margin:0;
1099 1101 line-height:16px;
1100 1102 white-space:nowrap;
1101 1103 }
1102 1104
1103 1105 .task.label {width:100%;}
1104 1106 .task.label.project, .task.label.version { font-weight: bold; }
1105 1107
1106 1108 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1107 1109 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1108 1110 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1109 1111
1110 1112 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1111 1113 .task_late.parent, .task_done.parent { height: 3px;}
1112 1114 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1113 1115 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1114 1116
1115 1117 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1116 1118 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1117 1119 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1118 1120 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1119 1121
1120 1122 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1121 1123 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1122 1124 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1123 1125 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1124 1126
1125 1127 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1126 1128 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1127 1129
1128 1130 /***** Icons *****/
1129 1131 .icon {
1130 1132 background-position: 0% 50%;
1131 1133 background-repeat: no-repeat;
1132 1134 padding-left: 20px;
1133 1135 padding-top: 2px;
1134 1136 padding-bottom: 3px;
1135 1137 }
1136 1138 .icon-only {
1137 1139 background-position: 0% 50%;
1138 1140 background-repeat: no-repeat;
1139 1141 padding-left: 16px;
1140 }
1141 a.icon-only {
1142 1142 display: inline-block;
1143 1143 width: 0;
1144 1144 height: 16px;
1145 1145 overflow: hidden;
1146 1146 padding-top: 0;
1147 1147 padding-bottom: 0;
1148 1148 font-size: 8px;
1149 1149 vertical-align: text-bottom;
1150 1150 }
1151 a.icon-only::after {
1151 .icon-only::after {
1152 1152 content: "&nbsp;";
1153 1153 }
1154 1154
1155 1155 .icon-add { background-image: url(../images/add.png); }
1156 1156 .icon-edit { background-image: url(../images/edit.png); }
1157 1157 .icon-copy { background-image: url(../images/copy.png); }
1158 1158 .icon-duplicate { background-image: url(../images/duplicate.png); }
1159 1159 .icon-del { background-image: url(../images/delete.png); }
1160 1160 .icon-move { background-image: url(../images/move.png); }
1161 1161 .icon-save { background-image: url(../images/save.png); }
1162 1162 .icon-cancel { background-image: url(../images/cancel.png); }
1163 1163 .icon-multiple { background-image: url(../images/table_multiple.png); }
1164 1164 .icon-folder { background-image: url(../images/folder.png); }
1165 1165 .open .icon-folder { background-image: url(../images/folder_open.png); }
1166 1166 .icon-package { background-image: url(../images/package.png); }
1167 1167 .icon-user { background-image: url(../images/user.png); }
1168 1168 .icon-projects { background-image: url(../images/projects.png); }
1169 1169 .icon-help { background-image: url(../images/help.png); }
1170 1170 .icon-attachment { background-image: url(../images/attachment.png); }
1171 1171 .icon-history { background-image: url(../images/history.png); }
1172 1172 .icon-time { background-image: url(../images/time.png); }
1173 1173 .icon-time-add { background-image: url(../images/time_add.png); }
1174 1174 .icon-stats { background-image: url(../images/stats.png); }
1175 1175 .icon-warning { background-image: url(../images/warning.png); }
1176 1176 .icon-error { background-image: url(../images/exclamation.png); }
1177 1177 .icon-fav { background-image: url(../images/fav.png); }
1178 1178 .icon-fav-off { background-image: url(../images/fav_off.png); }
1179 1179 .icon-reload { background-image: url(../images/reload.png); }
1180 1180 .icon-lock { background-image: url(../images/locked.png); }
1181 1181 .icon-unlock { background-image: url(../images/unlock.png); }
1182 1182 .icon-checked { background-image: url(../images/toggle_check.png); }
1183 1183 .icon-details { background-image: url(../images/zoom_in.png); }
1184 1184 .icon-report { background-image: url(../images/report.png); }
1185 1185 .icon-comment { background-image: url(../images/comment.png); }
1186 1186 .icon-summary { background-image: url(../images/lightning.png); }
1187 1187 .icon-server-authentication { background-image: url(../images/server_key.png); }
1188 1188 .icon-issue { background-image: url(../images/ticket.png); }
1189 1189 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1190 1190 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1191 1191 .icon-magnifier { background-image: url(../images/magnifier.png); }
1192 1192 .icon-passwd { background-image: url(../images/textfield_key.png); }
1193 1193 .icon-test { background-image: url(../images/bullet_go.png); }
1194 1194 .icon-email { background-image: url(../images/email.png); }
1195 1195 .icon-email-disabled { background-image: url(../images/email_disabled.png); }
1196 1196 .icon-email-add { background-image: url(../images/email_add.png); }
1197 1197 .icon-move-up { background-image: url(../images/1uparrow.png); }
1198 1198 .icon-move-top { background-image: url(../images/2uparrow.png); }
1199 1199 .icon-move-down { background-image: url(../images/1downarrow.png); }
1200 1200 .icon-move-bottom { background-image: url(../images/2downarrow.png); }
1201 1201 .icon-ok { background-image: url(../images/true.png); }
1202 1202 .icon-not-ok { background-image: url(../images/false.png); }
1203 1203 .icon-link-break { background-image: url(../images/link_break.png); }
1204 1204 .icon-list { background-image: url(../images/text_list_bullets.png); }
1205 1205
1206 1206 .icon-file { background-image: url(../images/files/default.png); }
1207 1207 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1208 1208 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1209 1209 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1210 1210 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1211 1211 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1212 1212 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1213 1213 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1214 1214 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1215 1215 .icon-file.text-css { background-image: url(../images/files/css.png); }
1216 1216 .icon-file.text-html { background-image: url(../images/files/html.png); }
1217 1217 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1218 1218 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1219 1219 .icon-file.image-png { background-image: url(../images/files/image.png); }
1220 1220 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1221 1221 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1222 1222 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1223 1223 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1224 1224
1225 1225 .sort-handle { width:16px; height:16px; background:url(../images/reorder.png) no-repeat 0 50%; cursor:move; }
1226 1226 .sort-handle.ajax-loading { background-image: url(../images/loading.gif); }
1227 1227 tr.ui-sortable-helper { border:1px solid #e4e4e4; }
1228 1228
1229 1229 .contextual>.icon:not(:first-child), .buttons>.icon:not(:first-child) { margin-left: 5px; }
1230 1230
1231 1231 img.gravatar {
1232 1232 vertical-align: middle;
1233 1233 border-radius: 20%;
1234 1234 }
1235 1235
1236 1236 div.issue img.gravatar {
1237 1237 float: left;
1238 1238 margin: 0 6px 0 0;
1239 1239 }
1240 1240
1241 1241 h2 img.gravatar {margin: -2px 4px -4px 0;}
1242 1242 h3 img.gravatar {margin: -4px 4px -4px 0;}
1243 1243 h4 img.gravatar {margin: -6px 4px -4px 0;}
1244 1244 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1245 1245 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1246 1246 /* Used on 12px Gravatar img tags without the icon background */
1247 1247 .icon-gravatar {float: left; margin-right: 4px;}
1248 1248
1249 1249 #activity dt, .journal {clear: left;}
1250 1250
1251 1251 .journal-link {float: right;}
1252 1252
1253 1253 h2 img { vertical-align:middle; }
1254 1254
1255 1255 .hascontextmenu { cursor: context-menu; }
1256 1256
1257 1257 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1258 1258 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1259 1259 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1260 1260
1261 1261 .ui-progressbar {position: relative;}
1262 1262 #progress-label {
1263 1263 position: absolute; left: 50%; top: 4px;
1264 1264 font-weight: bold;
1265 1265 color: #555; text-shadow: 1px 1px 0 #fff;
1266 1266 }
1267 1267
1268 1268 /* Custom JQuery styles */
1269 1269 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1270 1270
1271 1271
1272 1272 /************* CodeRay styles *************/
1273 1273 .syntaxhl div {display: inline;}
1274 1274 .syntaxhl .code pre { overflow: auto }
1275 1275
1276 1276 .syntaxhl .annotation { color:#007 }
1277 1277 .syntaxhl .attribute-name { color:#b48 }
1278 1278 .syntaxhl .attribute-value { color:#700 }
1279 1279 .syntaxhl .binary { color:#549 }
1280 1280 .syntaxhl .binary .char { color:#325 }
1281 1281 .syntaxhl .binary .delimiter { color:#325 }
1282 1282 .syntaxhl .char { color:#D20 }
1283 1283 .syntaxhl .char .content { color:#D20 }
1284 1284 .syntaxhl .char .delimiter { color:#710 }
1285 1285 .syntaxhl .class { color:#B06; font-weight:bold }
1286 1286 .syntaxhl .class-variable { color:#369 }
1287 1287 .syntaxhl .color { color:#0A0 }
1288 1288 .syntaxhl .comment { color:#777 }
1289 1289 .syntaxhl .comment .char { color:#444 }
1290 1290 .syntaxhl .comment .delimiter { color:#444 }
1291 1291 .syntaxhl .constant { color:#036; font-weight:bold }
1292 1292 .syntaxhl .decorator { color:#B0B }
1293 1293 .syntaxhl .definition { color:#099; font-weight:bold }
1294 1294 .syntaxhl .delimiter { color:black }
1295 1295 .syntaxhl .directive { color:#088; font-weight:bold }
1296 1296 .syntaxhl .docstring { color:#D42; }
1297 1297 .syntaxhl .doctype { color:#34b }
1298 1298 .syntaxhl .done { text-decoration: line-through; color: gray }
1299 1299 .syntaxhl .entity { color:#800; font-weight:bold }
1300 1300 .syntaxhl .error { color:#F00; background-color:#FAA }
1301 1301 .syntaxhl .escape { color:#666 }
1302 1302 .syntaxhl .exception { color:#C00; font-weight:bold }
1303 1303 .syntaxhl .float { color:#60E }
1304 1304 .syntaxhl .function { color:#06B; font-weight:bold }
1305 1305 .syntaxhl .function .delimiter { color:#059 }
1306 1306 .syntaxhl .function .content { color:#037 }
1307 1307 .syntaxhl .global-variable { color:#d70 }
1308 1308 .syntaxhl .hex { color:#02b }
1309 1309 .syntaxhl .id { color:#33D; font-weight:bold }
1310 1310 .syntaxhl .include { color:#B44; font-weight:bold }
1311 1311 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1312 1312 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1313 1313 .syntaxhl .instance-variable { color:#33B }
1314 1314 .syntaxhl .integer { color:#00D }
1315 1315 .syntaxhl .imaginary { color:#f00 }
1316 1316 .syntaxhl .important { color:#D00 }
1317 1317 .syntaxhl .key { color: #606 }
1318 1318 .syntaxhl .key .char { color: #60f }
1319 1319 .syntaxhl .key .delimiter { color: #404 }
1320 1320 .syntaxhl .keyword { color:#080; font-weight:bold }
1321 1321 .syntaxhl .label { color:#970; font-weight:bold }
1322 1322 .syntaxhl .local-variable { color:#950 }
1323 1323 .syntaxhl .map .content { color:#808 }
1324 1324 .syntaxhl .map .delimiter { color:#40A}
1325 1325 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1326 1326 .syntaxhl .namespace { color:#707; font-weight:bold }
1327 1327 .syntaxhl .octal { color:#40E }
1328 1328 .syntaxhl .operator { }
1329 1329 .syntaxhl .predefined { color:#369; font-weight:bold }
1330 1330 .syntaxhl .predefined-constant { color:#069 }
1331 1331 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1332 1332 .syntaxhl .preprocessor { color:#579 }
1333 1333 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1334 1334 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1335 1335 .syntaxhl .regexp .content { color:#808 }
1336 1336 .syntaxhl .regexp .delimiter { color:#404 }
1337 1337 .syntaxhl .regexp .modifier { color:#C2C }
1338 1338 .syntaxhl .reserved { color:#080; font-weight:bold }
1339 1339 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1340 1340 .syntaxhl .shell .content { color:#2B2 }
1341 1341 .syntaxhl .shell .delimiter { color:#161 }
1342 1342 .syntaxhl .string { background-color:hsla(0,100%,50%,0.05); }
1343 1343 .syntaxhl .string .char { color: #b0b }
1344 1344 .syntaxhl .string .content { color: #D20 }
1345 1345 .syntaxhl .string .delimiter { color: #710 }
1346 1346 .syntaxhl .string .modifier { color: #E40 }
1347 1347 .syntaxhl .symbol { color:#A60 }
1348 1348 .syntaxhl .symbol .content { color:#A60 }
1349 1349 .syntaxhl .symbol .delimiter { color:#740 }
1350 1350 .syntaxhl .tag { color:#070; font-weight:bold }
1351 1351 .syntaxhl .type { color:#339; font-weight:bold }
1352 1352 .syntaxhl .value { color: #088 }
1353 1353 .syntaxhl .variable { color:#037 }
1354 1354
1355 1355 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1356 1356 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1357 1357 .syntaxhl .change { color: #bbf; background: #007 }
1358 1358 .syntaxhl .head { color: #f8f; background: #505 }
1359 1359 .syntaxhl .head .filename { color: white; }
1360 1360
1361 1361 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1362 1362 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1363 1363
1364 1364 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1365 1365 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1366 1366 .syntaxhl .change .change { color: #88f }
1367 1367 .syntaxhl .head .head { color: #f4f }
1368 1368
1369 1369 /***** Media print specific styles *****/
1370 1370 @media print {
1371 1371 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1372 1372 #main { background: #fff; }
1373 1373 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1374 1374 #wiki_add_attachment { display:none; }
1375 1375 .hide-when-print, .pagination ul.pages, .pagination .per-page { display: none !important; }
1376 1376 .autoscroll {overflow-x: visible;}
1377 1377 table.list {margin-top:0.5em;}
1378 1378 table.list th, table.list td {border: 1px solid #aaa;}
1379 1379 }
1380 1380
1381 1381 /* Accessibility specific styles */
1382 1382 .hidden-for-sighted {
1383 1383 position:absolute;
1384 1384 left:-10000px;
1385 1385 top:auto;
1386 1386 width:1px;
1387 1387 height:1px;
1388 1388 overflow:hidden;
1389 1389 }
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now