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