Patch #2268 » total_estimated_hour_2_6_4.diff
app/helpers/issues_helper.rb (working copy) | ||
---|---|---|
427 | 427 |
end |
428 | 428 |
end |
429 | 429 |
end |
430 | ||
431 |
def estimated_done(issues) |
|
432 |
issues.map(&:estimated_done).reject{|x|x.nil?}.sum.round(2) |
|
433 |
end |
|
434 | ||
435 |
def estimated_hours(issues) |
|
436 |
issues.map(&:estimated_hours).reject{|x| x.nil?}.sum |
|
437 |
end |
|
438 | ||
439 |
def estimated_done_percentage(issues) |
|
440 |
(100 * estimated_done(issues) / estimated_hours(issues)).round(2) |
|
441 |
end |
|
430 | 442 |
end |
app/helpers/queries_helper.rb (working copy) | ||
---|---|---|
18 | 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | 19 | |
20 | 20 |
module QueriesHelper |
21 |
include ApplicationHelper |
|
22 | ||
23 | 21 |
def filters_options_for_select(query) |
24 | 22 |
options_for_select(filters_options(query)) |
25 | 23 |
end |
... | ... | |
83 | 81 |
end |
84 | 82 | |
85 | 83 |
def column_content(column, issue) |
86 |
value = column.value_object(issue)
|
|
84 |
value = column.value(issue) |
|
87 | 85 |
if value.is_a?(Array) |
88 | 86 |
value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe |
89 | 87 |
else |
... | ... | |
97 | 95 |
link_to value, issue_path(issue) |
98 | 96 |
when :subject |
99 | 97 |
link_to value, issue_path(issue) |
100 |
when :parent |
|
101 |
value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : '' |
|
102 | 98 |
when :description |
103 | 99 |
issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' |
104 | 100 |
when :done_ratio |
105 | 101 |
progress_bar(value, :width => '80px') |
102 |
when :estimated_done |
|
103 |
if (value.nil?) |
|
104 |
value = 0 |
|
105 |
end |
|
106 |
sprintf "%.2f", value |
|
106 | 107 |
when :relations |
107 | 108 |
other = value.other_issue(issue) |
108 | 109 |
content_tag('span', |
109 | 110 |
(l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe, |
110 | 111 |
:class => value.css_classes_for(issue)) |
111 |
else |
|
112 |
else
|
|
112 | 113 |
format_object(value) |
113 |
end |
|
114 |
end
|
|
114 | 115 |
end |
115 | 116 | |
116 | 117 |
def csv_content(column, issue) |
117 |
value = column.value_object(issue)
|
|
118 |
value = column.value(issue) |
|
118 | 119 |
if value.is_a?(Array) |
119 | 120 |
value.collect {|v| csv_value(column, issue, v)}.compact.join(', ') |
120 |
else |
|
121 |
else
|
|
121 | 122 |
csv_value(column, issue, value) |
122 |
end |
|
123 |
end
|
|
123 | 124 |
end |
124 | 125 | |
125 |
def csv_value(column, object, value)
|
|
126 |
format_object(value, false) do |value|
|
|
127 |
case value.class.name
|
|
128 |
when 'Float'
|
|
129 |
sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
|
|
130 |
when 'IssueRelation'
|
|
131 |
other = value.other_issue(object)
|
|
132 |
l(value.label_for(object)) + " ##{other.id}"
|
|
133 |
when 'Issue'
|
|
134 |
if object.is_a?(TimeEntry)
|
|
135 |
"#{value.tracker} ##{value.id}: #{value.subject}"
|
|
136 |
else
|
|
137 |
value.id
|
|
138 |
end
|
|
139 |
else
|
|
140 |
value
|
|
141 |
end
|
|
126 |
def csv_value(column, issue, value)
|
|
127 |
case value.class.name
|
|
128 |
when 'Time'
|
|
129 |
format_time(value)
|
|
130 |
when 'Date'
|
|
131 |
format_date(value)
|
|
132 |
when 'Float'
|
|
133 |
sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
|
|
134 |
when 'IssueRelation'
|
|
135 |
other = value.other_issue(issue)
|
|
136 |
l(value.label_for(issue)) + " ##{other.id}"
|
|
137 |
when 'TrueClass'
|
|
138 |
l(:general_text_Yes)
|
|
139 |
when 'FalseClass'
|
|
140 |
l(:general_text_No)
|
|
141 |
else
|
|
142 |
value.to_s
|
|
142 | 143 |
end |
143 | 144 |
end |
144 | 145 |
app/models/query.rb (working copy) | ||
---|---|---|
57 | 57 |
object.send name |
58 | 58 |
end |
59 | 59 | |
60 |
def value_object(object) |
|
61 |
object.send name |
|
62 |
end |
|
63 | ||
64 | 60 |
def css_classes |
65 | 61 |
name |
66 | 62 |
end |
... | ... | |
84 | 80 |
@cf |
85 | 81 |
end |
86 | 82 | |
87 |
def value_object(object)
|
|
83 |
def value(object) |
|
88 | 84 |
if custom_field.visible_by?(object.project, User.current) |
89 |
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id} |
|
90 |
cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
|
|
85 |
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
|
86 |
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
|
91 | 87 |
else |
92 | 88 |
nil |
93 |
end |
|
94 | 89 |
end |
95 | ||
96 |
def value(object) |
|
97 |
raw = value_object(object) |
|
98 |
if raw.is_a?(Array) |
|
99 |
raw.map {|r| @cf.cast_value(r.value)} |
|
100 |
elsif raw |
|
101 |
@cf.cast_value(raw.value) |
|
102 |
else |
|
103 |
nil |
|
104 |
end |
|
105 | 90 |
end |
106 | 91 | |
107 | 92 |
def css_classes |
... | ... | |
120 | 105 |
@association = association |
121 | 106 |
end |
122 | 107 | |
123 |
def value_object(object)
|
|
108 |
def value(object) |
|
124 | 109 |
if assoc = object.send(@association) |
125 | 110 |
super(assoc) |
126 | 111 |
end |
... | ... | |
159 | 144 | |
160 | 145 |
after_save do |query| |
161 | 146 |
if query.visibility_changed? && query.visibility != VISIBILITY_ROLES |
162 |
query.roles.clear
|
|
163 |
end
|
|
147 |
query.roles.clear
|
|
148 |
end
|
|
164 | 149 |
end |
165 | 150 | |
166 | 151 |
class_attribute :operators |
167 | 152 |
self.operators = { |
168 | 153 |
"=" => :label_equals, |
169 |
"!" => :label_not_equals, |
|
170 |
"o" => :label_open_issues, |
|
171 |
"c" => :label_closed_issues, |
|
172 |
"!*" => :label_none, |
|
154 |
"!" => :label_not_equals,
|
|
155 |
"o" => :label_open_issues,
|
|
156 |
"c" => :label_closed_issues,
|
|
157 |
"!*" => :label_none,
|
|
173 | 158 |
"*" => :label_any, |
174 |
">=" => :label_greater_or_equal, |
|
175 |
"<=" => :label_less_or_equal, |
|
176 |
"><" => :label_between, |
|
177 |
"<t+" => :label_in_less_than, |
|
178 |
">t+" => :label_in_more_than, |
|
159 |
">=" => :label_greater_or_equal,
|
|
160 |
"<=" => :label_less_or_equal,
|
|
161 |
"><" => :label_between,
|
|
162 |
"<t+" => :label_in_less_than,
|
|
163 |
">t+" => :label_in_more_than,
|
|
179 | 164 |
"><t+"=> :label_in_the_next_days, |
180 |
"t+" => :label_in, |
|
181 |
"t" => :label_today, |
|
165 |
"t+" => :label_in,
|
|
166 |
"t" => :label_today,
|
|
182 | 167 |
"ld" => :label_yesterday, |
183 |
"w" => :label_this_week, |
|
168 |
"w" => :label_this_week,
|
|
184 | 169 |
"lw" => :label_last_week, |
185 | 170 |
"l2w" => [:label_last_n_weeks, {:count => 2}], |
186 | 171 |
"m" => :label_this_month, |
187 | 172 |
"lm" => :label_last_month, |
188 | 173 |
"y" => :label_this_year, |
189 |
">t-" => :label_less_than_ago, |
|
190 |
"<t-" => :label_more_than_ago, |
|
174 |
">t-" => :label_less_than_ago,
|
|
175 |
"<t-" => :label_more_than_ago,
|
|
191 | 176 |
"><t-"=> :label_in_the_past_days, |
192 |
"t-" => :label_ago, |
|
193 |
"~" => :label_contains, |
|
177 |
"t-" => :label_ago,
|
|
178 |
"~" => :label_contains,
|
|
194 | 179 |
"!~" => :label_not_contains, |
195 | 180 |
"=p" => :label_any_issues_in_project, |
196 | 181 |
"=!p" => :label_any_issues_not_in_project, |
... | ... | |
200 | 185 |
class_attribute :operators_by_filter_type |
201 | 186 |
self.operators_by_filter_type = { |
202 | 187 |
:list => [ "=", "!" ], |
203 |
:list_status => [ "o", "=", "!", "c", "*" ], |
|
204 |
:list_optional => [ "=", "!", "!*", "*" ], |
|
205 |
:list_subprojects => [ "*", "!*", "=" ], |
|
188 |
:list_status => [ "o", "=", "!", "c", "*" ],
|
|
189 |
:list_optional => [ "=", "!", "!*", "*" ],
|
|
190 |
:list_subprojects => [ "*", "!*", "=" ],
|
|
206 | 191 |
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ], |
207 | 192 |
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ], |
208 |
:string => [ "=", "~", "!", "!~", "!*", "*" ], |
|
209 |
:text => [ "~", "!~", "!*", "*" ], |
|
210 |
:integer => [ "=", ">=", "<=", "><", "!*", "*" ], |
|
193 |
:string => [ "=", "~", "!", "!~", "!*", "*" ],
|
|
194 |
:text => [ "~", "!~", "!*", "*" ],
|
|
195 |
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
|
|
211 | 196 |
:float => [ "=", ">=", "<=", "><", "!*", "*" ], |
212 | 197 |
:relation => ["=", "=p", "=!p", "!p", "!*", "*"] |
213 | 198 |
} |
... | ... | |
301 | 286 |
json = {} |
302 | 287 |
available_filters.each do |field, options| |
303 | 288 |
json[field] = options.slice(:type, :name, :values).stringify_keys |
304 |
end |
|
289 |
end
|
|
305 | 290 |
json |
306 |
end |
|
291 |
end
|
|
307 | 292 | |
308 | 293 |
def all_projects |
309 | 294 |
@all_projects ||= Project.visible.all |
310 |
end |
|
295 |
end
|
|
311 | 296 | |
312 | 297 |
def all_projects_values |
313 | 298 |
return @all_projects_values if @all_projects_values |
314 | 299 | |
315 | 300 |
values = [] |
316 |
Project.project_tree(all_projects) do |p, level| |
|
317 |
prefix = (level > 0 ? ('--' * level + ' ') : '') |
|
301 |
Project.project_tree(all_projects) do |p, level|
|
|
302 |
prefix = (level > 0 ? ('--' * level + ' ') : '')
|
|
318 | 303 |
values << ["#{prefix}#{p.name}", p.id.to_s] |
319 |
end |
|
304 |
end
|
|
320 | 305 |
@all_projects_values = values |
321 |
end |
|
306 |
end
|
|
322 | 307 | |
323 | 308 |
# Adds available filters |
324 | 309 |
def initialize_available_filters |
325 | 310 |
# implemented by sub-classes |
326 |
end |
|
311 |
end
|
|
327 | 312 |
protected :initialize_available_filters |
328 | 313 | |
329 | 314 |
# Adds an available filter |
... | ... | |
331 | 316 |
@available_filters ||= ActiveSupport::OrderedHash.new |
332 | 317 |
@available_filters[field] = options |
333 | 318 |
@available_filters |
334 |
end |
|
319 |
end
|
|
335 | 320 | |
336 | 321 |
# Removes an available filter |
337 | 322 |
def delete_available_filter(field) |
338 | 323 |
if @available_filters |
339 | 324 |
@available_filters.delete(field) |
340 |
end |
|
341 |
end |
|
325 |
end
|
|
326 |
end
|
|
342 | 327 | |
343 | 328 |
# Return a hash of available filters |
344 | 329 |
def available_filters |
... | ... | |
417 | 402 |
# Returns a Hash of columns and the key for sorting |
418 | 403 |
def sortable_columns |
419 | 404 |
available_columns.inject({}) {|h, column| |
420 |
h[column.name.to_s] = column.sortable |
|
421 |
h |
|
405 |
h[column.name.to_s] = column.sortable
|
|
406 |
h
|
|
422 | 407 |
} |
423 | 408 |
end |
424 | 409 | |
... | ... | |
436 | 421 | |
437 | 422 |
def block_columns |
438 | 423 |
columns.reject(&:inline?) |
439 |
end |
|
424 |
end
|
|
440 | 425 | |
441 | 426 |
def available_inline_columns |
442 | 427 |
available_columns.select(&:inline?) |
... | ... | |
556 | 541 |
next unless v and !v.empty? |
557 | 542 |
operator = operator_for(field) |
558 | 543 | |
559 |
# "me" value substitution
|
|
544 |
# "me" value subsitution |
|
560 | 545 |
if %w(assigned_to_id author_id user_id watcher_id).include?(field) |
561 | 546 |
if v.delete("me") |
562 | 547 |
if User.current.logged? |
... | ... | |
622 | 607 |
customized_key = "#{assoc}_id" |
623 | 608 |
customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil |
624 | 609 |
raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class |
625 |
end
|
|
610 |
end |
|
626 | 611 |
where = sql_for_field(field, operator, value, db_table, db_field, true) |
627 | 612 |
if operator =~ /[<>]/ |
628 | 613 |
where = "(#{where}) AND #{db_table}.#{db_field} <> ''" |
... | ... | |
793 | 778 |
if assoc.present? |
794 | 779 |
filter_id = "#{assoc}.#{filter_id}" |
795 | 780 |
filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) |
796 |
end |
|
781 |
end
|
|
797 | 782 |
add_available_filter filter_id, options.merge({ |
798 | 783 |
:name => filter_name, |
799 | 784 |
:field => field |
800 | 785 |
}) |
801 |
end |
|
786 |
end
|
|
802 | 787 | |
803 | 788 |
# Adds filters for the given custom fields scope |
804 | 789 |
def add_custom_fields_filters(scope, assoc=nil) |
app/models/issue_query.rb (working copy) | ||
---|---|---|
38 | 38 |
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), |
39 | 39 |
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
40 | 40 |
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), |
41 |
QueryColumn.new(:estimated_done, :sortable => "#{Issue.table_name}.estimated_done", :caption => :field_estimated_done), |
|
41 | 42 |
QueryColumn.new(:relations, :caption => :label_related_issues), |
42 | 43 |
QueryColumn.new(:description, :inline => false) |
43 | 44 |
] |
... | ... | |
147 | 148 |
end |
148 | 149 |
principals.uniq! |
149 | 150 |
principals.sort! |
150 |
principals.reject! {|p| p.is_a?(GroupBuiltin)} |
|
151 | 151 |
users = principals.select {|p| p.is_a?(User)} |
152 | 152 | |
153 | 153 |
add_available_filter "status_id", |
... | ... | |
184 | 184 |
:type => :list_optional, :values => assigned_to_values |
185 | 185 |
) unless assigned_to_values.empty? |
186 | 186 | |
187 |
group_values = Group.givable.collect {|g| [g.name, g.id.to_s] }
|
|
187 |
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
|
|
188 | 188 |
add_available_filter("member_of_group", |
189 | 189 |
:type => :list_optional, :values => group_values |
190 | 190 |
) unless group_values.empty? |
... | ... | |
214 | 214 |
add_available_filter "due_date", :type => :date |
215 | 215 |
add_available_filter "estimated_hours", :type => :float |
216 | 216 |
add_available_filter "done_ratio", :type => :integer |
217 |
add_available_filter "estimated_done", :type => :float |
|
217 | 218 | |
218 | 219 |
if User.current.allowed_to?(:set_issues_private, nil, :global => true) || |
219 | 220 |
User.current.allowed_to?(:set_own_issues_private, nil, :global => true) |
... | ... | |
293 | 294 |
rescue ::ActiveRecord::StatementInvalid => e |
294 | 295 |
raise StatementInvalid.new(e.message) |
295 | 296 |
end |
297 |
|
|
298 |
# Returns sum of all the issue's estimated_hours |
|
299 |
def issue_sum |
|
300 |
Issue.visible.sum(:estimated_hours, :include => [:status, :project], :conditions => statement) |
|
301 |
rescue ::ActiveRecord::StatementInvalid => e |
|
302 |
raise StatementInvalid.new(e.message) |
|
303 |
end |
|
296 | 304 | |
305 |
# Returns sum of all the issue's estimated_done |
|
306 |
def issue_sum_in_progress |
|
307 |
Issue.visible.sum(:estimated_done, :include => [:status, :project], :conditions => statement) |
|
308 |
rescue ::ActiveRecord::StatementInvalid => e |
|
309 |
raise StatementInvalid.new(e.message) |
|
310 |
end |
|
311 | ||
297 | 312 |
# Returns the issue count by group or nil if query is not grouped |
298 | 313 |
def issue_count_by_group |
299 | 314 |
r = nil |
... | ... | |
318 | 333 |
rescue ::ActiveRecord::StatementInvalid => e |
319 | 334 |
raise StatementInvalid.new(e.message) |
320 | 335 |
end |
321 | ||
336 |
|
|
337 |
# Returns sum of the issue's estimated_hours by group or nil if query is not grouped |
|
338 |
def issue_sum_by_group |
|
339 |
r = nil |
|
340 |
if grouped? |
|
341 |
begin |
|
342 |
r = Issue.visible.sum(:estimated_hours, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) |
|
343 |
rescue ActiveRecord::RecordNotFound |
|
344 |
r= {r => issue_sum} |
|
345 |
end |
|
346 |
|
|
347 |
c = group_by_column |
|
348 |
if c.is_a?(QueryCustomFieldColumn) |
|
349 |
r = r.keys.inject({}) {|h,k| h[c.custom_field.cast_value(k)] = r[k]; h} |
|
350 |
end |
|
351 |
end |
|
352 |
r |
|
353 |
rescue ::ActiveRecord::StatementInvalid => e |
|
354 |
raise StatementInvalid.new(e.message) |
|
355 |
end |
|
356 |
|
|
357 |
# Returns sum of the issue's estimated_done by group or nil if query is not grouped |
|
358 |
def issue_progress_by_group |
|
359 |
r = nil |
|
360 |
if grouped? |
|
361 |
begin |
|
362 |
r = Issue.visible.sum(:estimated_done, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) |
|
363 |
rescue ActiveRecord::RecordNotFound |
|
364 |
r= {r => issue_sum_by_group} |
|
365 |
end |
|
366 |
|
|
367 |
c = group_by_column |
|
368 |
if c.is_a?(QueryCustomFieldColumn) |
|
369 |
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} |
|
370 |
end |
|
371 |
end |
|
372 |
r |
|
373 |
rescue ::ActiveRecord::StatementInvalid => e |
|
374 |
raise StatementInvalid.new(e.message) |
|
375 |
end |
|
376 |
|
|
322 | 377 |
# Returns the issues |
323 | 378 |
# Valid options are :order, :offset, :limit, :include, :conditions |
324 | 379 |
def issues(options={}) |
... | ... | |
405 | 460 | |
406 | 461 |
def sql_for_member_of_group_field(field, operator, value) |
407 | 462 |
if operator == '*' # Any group |
408 |
groups = Group.givable
|
|
463 |
groups = Group.all
|
|
409 | 464 |
operator = '=' # Override the operator since we want to find by assigned_to |
410 | 465 |
elsif operator == "!*" |
411 |
groups = Group.givable
|
|
466 |
groups = Group.all
|
|
412 | 467 |
operator = '!' # Override the operator since we want to find by assigned_to |
413 | 468 |
else |
414 | 469 |
groups = Group.where(:id => value).all |
app/models/issue.rb (working copy) | ||
---|---|---|
33 | 33 |
has_many :visible_journals, |
34 | 34 |
:class_name => 'Journal', |
35 | 35 |
:as => :journalized, |
36 |
:conditions => Proc.new { |
|
36 |
:conditions => Proc.new {
|
|
37 | 37 |
["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false] |
38 | 38 |
}, |
39 | 39 |
:readonly => true |
... | ... | |
94 | 94 |
before_create :default_assign |
95 | 95 |
before_save :close_duplicates, :update_done_ratio_from_issue_status, |
96 | 96 |
:force_updated_on_change, :update_closed_on, :set_assigned_to_was |
97 |
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} |
|
97 |
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
|
|
98 | 98 |
after_save :reschedule_following_issues, :update_nested_set_attributes, |
99 | 99 |
:update_parent_attributes, :create_journal |
100 | 100 |
# Should be after_create but would be called before previous after_save callbacks |
... | ... | |
122 | 122 |
end |
123 | 123 |
else |
124 | 124 |
"(#{table_name}.is_private = #{connection.quoted_false})" |
125 |
end |
|
126 | 125 |
end |
127 | 126 |
end |
127 |
end |
|
128 | 128 | |
129 | 129 |
# Returns true if usr or current user is allowed to view the issue |
130 | 130 |
def visible?(usr=nil) |
... | ... | |
142 | 142 |
end |
143 | 143 |
else |
144 | 144 |
!self.is_private? |
145 |
end |
|
146 | 145 |
end |
147 | 146 |
end |
147 |
end |
|
148 | 148 | |
149 | 149 |
# Returns true if user or current user is allowed to edit or add a note to the issue |
150 | 150 |
def editable?(user=User.current) |
... | ... | |
195 | 195 |
@workflow_rule_by_attribute = nil |
196 | 196 |
@assignable_versions = nil |
197 | 197 |
@relations = nil |
198 |
@spent_hours = nil |
|
199 | 198 |
base_reload(*args) |
200 | 199 |
end |
201 | 200 | |
... | ... | |
219 | 218 |
self.status = issue.status |
220 | 219 |
self.author = User.current |
221 | 220 |
unless options[:attachments] == false |
222 |
self.attachments = issue.attachments.map do |attachement| |
|
221 |
self.attachments = issue.attachments.map do |attachement|
|
|
223 | 222 |
attachement.copy(:container => self) |
224 | 223 |
end |
225 | 224 |
end |
... | ... | |
356 | 355 |
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) |
357 | 356 |
end |
358 | 357 | |
358 |
def estimated_done=(h) |
|
359 |
write_attribute :estimated_done, (h.is_a?(String) ? h.to_hours : h) |
|
360 |
end |
|
361 |
|
|
359 | 362 |
safe_attributes 'project_id', |
360 | 363 |
:if => lambda {|issue, user| |
361 | 364 |
if issue.new_record? |
... | ... | |
395 | 398 |
:if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} |
396 | 399 | |
397 | 400 |
safe_attributes 'private_notes', |
398 |
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} |
|
401 |
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
|
|
399 | 402 | |
400 | 403 |
safe_attributes 'watcher_user_ids', |
401 |
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} |
|
404 |
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
|
|
402 | 405 | |
403 | 406 |
safe_attributes 'is_private', |
404 | 407 |
:if => lambda {|issue, user| |
... | ... | |
454 | 457 |
s = attrs['parent_issue_id'].to_s |
455 | 458 |
unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) |
456 | 459 |
@invalid_parent_issue_id = attrs.delete('parent_issue_id') |
457 |
end |
|
458 | 460 |
end |
461 |
end |
|
459 | 462 | |
460 | 463 |
if attrs['custom_field_values'].present? |
461 | 464 |
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s} |
... | ... | |
530 | 533 |
return {} if roles.empty? |
531 | 534 | |
532 | 535 |
result = {} |
533 |
workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)) |
|
536 |
workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
|
|
534 | 537 |
if workflow_permissions.any? |
535 | 538 |
workflow_rules = workflow_permissions.inject({}) do |h, wp| |
536 | 539 |
h[wp.field_name] ||= {} |
... | ... | |
569 | 572 |
private :workflow_rule_by_attribute |
570 | 573 | |
571 | 574 |
def done_ratio |
572 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
|
575 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0
|
|
573 | 576 |
status.default_done_ratio |
574 | 577 |
else |
575 | 578 |
read_attribute(:done_ratio) |
... | ... | |
641 | 644 |
errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') |
642 | 645 |
end |
643 | 646 |
else |
644 |
if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
|
|
647 |
if respond_to?(attribute) && send(attribute).blank? |
|
645 | 648 |
errors.add attribute, :blank |
646 | 649 |
end |
647 | 650 |
end |
... | ... | |
651 | 654 |
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios |
652 | 655 |
# even if the user turns off the setting later |
653 | 656 |
def update_done_ratio_from_issue_status |
654 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
|
657 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0
|
|
655 | 658 |
self.done_ratio = status.default_done_ratio |
656 | 659 |
end |
657 | 660 |
end |
... | ... | |
757 | 760 |
elsif project_id_changed? |
758 | 761 |
if project.shared_versions.include?(fixed_version) |
759 | 762 |
versions << fixed_version |
760 |
end
|
|
763 |
end |
|
761 | 764 |
else |
762 | 765 |
versions << fixed_version |
763 | 766 |
end |
... | ... | |
770 | 773 |
!relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? |
771 | 774 |
end |
772 | 775 | |
776 |
def update_estimated_done |
|
777 |
if children.count < 1 |
|
778 |
x1 = Issue.find_by_id(id).done_ratio.to_f |
|
779 |
x2 = Issue.find_by_id(id).estimated_hours.to_f |
|
780 |
r = ((x1 * x2)/100).round(2) |
|
781 |
Issue.update(id, :estimated_done => r) |
|
782 |
end |
|
783 |
end |
|
784 |
|
|
773 | 785 |
# Returns an array of statuses that user is able to apply |
774 | 786 |
def new_statuses_allowed_to(user=User.current, include_default=false) |
775 | 787 |
if new_record? && @copied_from |
... | ... | |
784 | 796 |
initial_status ||= status |
785 | 797 | |
786 | 798 |
initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id |
787 |
assignee_transitions_allowed = initial_assigned_to_id.present? && |
|
799 |
assignee_transitions_allowed = initial_assigned_to_id.present? &&
|
|
788 | 800 |
(user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id)) |
789 | 801 | |
790 | 802 |
statuses = initial_status.find_new_statuses_allowed_to( |
... | ... | |
977 | 989 |
elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY) |
978 | 990 |
queue << child |
979 | 991 |
issue_status[child] = ePROCESS_ALL |
980 |
end
|
|
992 |
end |
|
981 | 993 |
end |
982 | 994 |
end |
983 | 995 | |
... | ... | |
1080 | 1092 |
# or if it starts before the given date |
1081 | 1093 |
if start_date == leaf.start_date || date > leaf.start_date |
1082 | 1094 |
leaf.reschedule_on!(date) |
1083 |
end
|
|
1095 |
end |
|
1084 | 1096 |
else |
1085 | 1097 |
leaf.reschedule_on!(date) |
1086 |
end |
|
1087 |
end |
|
1088 | 1098 |
end |
1089 | 1099 |
end |
1100 |
end |
|
1101 |
end |
|
1090 | 1102 | |
1091 | 1103 |
def <=>(issue) |
1092 | 1104 |
if issue.nil? |
... | ... | |
1129 | 1141 |
def self.update_versions_from_hierarchy_change(project) |
1130 | 1142 |
moved_project_ids = project.self_and_descendants.reload.collect(&:id) |
1131 | 1143 |
# Update issues of the moved projects and issues assigned to a version of a moved project |
1132 |
Issue.update_versions( |
|
1133 |
["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", |
|
1134 |
moved_project_ids, moved_project_ids] |
|
1135 |
) |
|
1144 |
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) |
|
1136 | 1145 |
end |
1137 | 1146 | |
1138 | 1147 |
def parent_issue_id=(arg) |
1139 | 1148 |
s = arg.to_s.strip.presence |
1140 | 1149 |
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) |
1150 |
@parent_issue.id |
|
1141 | 1151 |
@invalid_parent_issue_id = nil |
1142 | 1152 |
elsif s.blank? |
1143 | 1153 |
@parent_issue = nil |
... | ... | |
1177 | 1187 |
end |
1178 | 1188 |
end |
1179 | 1189 | |
1180 |
# Returns an issue scope based on project and scope |
|
1181 | 1190 |
def self.cross_project_scope(project, scope=nil) |
1182 | 1191 |
if project.nil? |
1183 | 1192 |
return Issue |
... | ... | |
1199 | 1208 |
end |
1200 | 1209 |
end |
1201 | 1210 | |
1211 | ||
1202 | 1212 |
# Extracted from the ReportsController. |
1203 | 1213 |
def self.by_tracker(project) |
1204 | 1214 |
count_and_group_by(:project => project, |
... | ... | |
1237 | 1247 |
end |
1238 | 1248 | |
1239 | 1249 |
def self.by_subproject(project) |
1240 |
ActiveRecord::Base.connection.select_all("select s.id as status_id, |
|
1241 |
s.is_closed as closed, |
|
1250 |
ActiveRecord::Base.connection.select_all("select s.id as status_id,
|
|
1251 |
s.is_closed as closed,
|
|
1242 | 1252 |
#{Issue.table_name}.project_id as project_id, |
1243 |
count(#{Issue.table_name}.id) as total |
|
1244 |
from |
|
1253 |
count(#{Issue.table_name}.id) as total
|
|
1254 |
from
|
|
1245 | 1255 |
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s |
1246 |
where |
|
1256 |
where
|
|
1247 | 1257 |
#{Issue.table_name}.status_id=s.id |
1248 | 1258 |
and #{Issue.table_name}.project_id = #{Project.table_name}.id |
1249 | 1259 |
and #{visible_condition(User.current, :project => project, :with_subprojects => true)} |
... | ... | |
1333 | 1343 |
if root_id.nil? |
1334 | 1344 |
# issue was just created |
1335 | 1345 |
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
1336 |
Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id]) |
|
1346 |
set_default_left_and_right |
|
1347 |
Issue.where(["id = ?", id]). |
|
1348 |
update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt]) |
|
1337 | 1349 |
if @parent_issue |
1338 | 1350 |
move_to_child_of(@parent_issue) |
1339 | 1351 |
end |
... | ... | |
1356 | 1368 |
move_to_right_of(root) |
1357 | 1369 |
end |
1358 | 1370 |
old_root_id = root_id |
1359 |
in_tenacious_transaction do |
|
1360 |
@parent_issue.reload_nested_set if @parent_issue |
|
1361 |
self.reload_nested_set |
|
1362 |
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
|
1363 |
cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt] |
|
1364 |
self.class.base_class.select('id').lock(true).where(cond) |
|
1365 |
offset = right_most_bound + 1 - lft |
|
1366 |
Issue.where(cond). |
|
1367 |
update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) |
|
1368 |
end |
|
1371 |
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id ) |
|
1372 |
target_maxright = nested_set_scope.maximum(right_column_name) || 0 |
|
1373 |
offset = target_maxright + 1 - lft |
|
1374 |
Issue.where(["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]). |
|
1375 |
update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) |
|
1376 |
self[left_column_name] = lft + offset |
|
1377 |
self[right_column_name] = rgt + offset |
|
1369 | 1378 |
if @parent_issue |
1370 | 1379 |
move_to_child_of(@parent_issue) |
1371 | 1380 |
end |
... | ... | |
1397 | 1406 |
if p.start_date && p.due_date && p.due_date < p.start_date |
1398 | 1407 |
p.start_date, p.due_date = p.due_date, p.start_date |
1399 | 1408 |
end |
1400 | ||
1409 |
|
|
1401 | 1410 |
# done ratio = weighted average ratio of leaves |
1402 | 1411 |
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio |
1403 | 1412 |
leaves_count = p.leaves.count |
... | ... | |
1406 | 1415 |
if average == 0 |
1407 | 1416 |
average = 1 |
1408 | 1417 |
end |
1409 |
done = p.leaves.joins(:status). |
|
1410 |
sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " + |
|
1411 |
"* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f |
|
1412 |
progress = done / (average * leaves_count) |
|
1413 |
p.done_ratio = progress.round |
|
1418 |
|
|
1419 |
#--original code-- |
|
1420 |
#change done_ratio to be the sum of children done_ratio |
|
1421 |
#done = p.leaves.joins(:status). |
|
1422 |
# sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " + |
|
1423 |
# "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f |
|
1424 |
#progress = done / (average * leaves_count) |
|
1425 |
#p.done_ratio = progress.round |
|
1426 |
#--end of original code--- |
|
1414 | 1427 |
end |
1428 | ||
1415 | 1429 |
end |
1416 | ||
1417 |
# estimate = sum of leaves estimates
|
|
1430 |
|
|
1431 |
# estimate = sum of leaves estimates
|
|
1418 | 1432 |
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f |
1433 |
p.estimated_done = p.leaves.sum(:estimated_done).to_f |
|
1434 |
|
|
1435 |
if (p.estimated_hours > 0) |
|
1436 |
p.done_ratio = (100 * p.estimated_done / p.estimated_hours).to_f.round(2) |
|
1437 |
end |
|
1438 |
|
|
1419 | 1439 |
p.estimated_hours = nil if p.estimated_hours == 0.0 |
1420 | ||
1440 |
p.estimated_done = nil if p.estimated_done == 0.0 |
|
1441 |
|
|
1421 | 1442 |
# ancestors will be recursively updated |
1422 | 1443 |
p.save(:validate => false) |
1423 | 1444 |
end |
... | ... | |
1477 | 1498 |
def close_duplicates |
1478 | 1499 |
if closing? |
1479 | 1500 |
duplicates.each do |duplicate| |
1480 |
# Reload is needed in case the duplicate was updated by a previous duplicate
|
|
1501 |
# Reload is need in case the duplicate was updated by a previous duplicate |
|
1481 | 1502 |
duplicate.reload |
1482 | 1503 |
# Don't re-close it if it's already closed |
1483 | 1504 |
next if duplicate.closed? |
... | ... | |
1497 | 1518 |
self.updated_on = current_time_from_proper_timezone |
1498 | 1519 |
if new_record? |
1499 | 1520 |
self.created_on = updated_on |
1500 |
end |
|
1501 | 1521 |
end |
1502 | 1522 |
end |
1523 |
end |
|
1503 | 1524 | |
1504 | 1525 |
# Callback for setting closed_on when the issue is closed. |
1505 | 1526 |
# The closed_on attribute stores the time of the last closing |
... | ... | |
1532 | 1553 |
before = @custom_values_before_change[c.custom_field_id] |
1533 | 1554 |
after = c.value |
1534 | 1555 |
next if before == after || (before.blank? && after.blank?) |
1535 | ||
1556 |
|
|
1536 | 1557 |
if before.is_a?(Array) || after.is_a?(Array) |
1537 | 1558 |
before = [before] unless before.is_a?(Array) |
1538 | 1559 |
after = [after] unless after.is_a?(Array) |
1539 | ||
1560 |
|
|
1540 | 1561 |
# values removed |
1541 | 1562 |
(before - after).reject(&:blank?).each do |value| |
1542 | 1563 |
@current_journal.details << JournalDetail.new(:property => 'cf', |
... | ... | |
1597 | 1618 | |
1598 | 1619 |
where = "#{Issue.table_name}.#{select_field}=j.id" |
1599 | 1620 | |
1600 |
ActiveRecord::Base.connection.select_all("select s.id as status_id, |
|
1601 |
s.is_closed as closed, |
|
1621 |
ActiveRecord::Base.connection.select_all("select s.id as status_id,
|
|
1622 |
s.is_closed as closed,
|
|
1602 | 1623 |
j.id as #{select_field}, |
1603 |
count(#{Issue.table_name}.id) as total |
|
1604 |
from |
|
1624 |
count(#{Issue.table_name}.id) as total
|
|
1625 |
from
|
|
1605 | 1626 |
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j |
1606 |
where |
|
1607 |
#{Issue.table_name}.status_id=s.id |
|
1627 |
where
|
|
1628 |
#{Issue.table_name}.status_id=s.id
|
|
1608 | 1629 |
and #{where} |
1609 | 1630 |
and #{Issue.table_name}.project_id=#{Project.table_name}.id |
1610 | 1631 |
and #{visible_condition(User.current, :project => project)} |
1611 | 1632 |
group by s.id, s.is_closed, j.id") |
1612 | 1633 |
end |
1613 | 1634 |
end |
1635 | ||
1636 |
app/controllers/issues_controller.rb (working copy) | ||
---|---|---|
81 | 81 |
:order => sort_clause, |
82 | 82 |
:offset => @offset, |
83 | 83 |
:limit => @limit) |
84 | ||
85 |
@all_issues = @query.issues(:include => [:status, :project, :assigned_to, :tracker, :priority, :category, :fixed_version]) |
|
86 |
|
|
84 | 87 |
@issue_count_by_group = @query.issue_count_by_group |
88 |
@issue_sum_by_group = @query.issue_sum_by_group |
|
89 |
@issue_progress_by_group = @query.issue_progress_by_group |
|
85 | 90 | |
86 | 91 |
respond_to do |format| |
87 | 92 |
format.html { render :template => 'issues/index', :layout => !request.xhr? } |
... | ... | |
148 | 153 |
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) |
149 | 154 |
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) |
150 | 155 |
if @issue.save |
156 |
@issue.update_estimated_done |
|
151 | 157 |
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) |
152 | 158 |
respond_to do |format| |
153 | 159 |
format.html { |
... | ... | |
195 | 201 |
end |
196 | 202 | |
197 | 203 |
if saved |
204 |
@issue.update_estimated_done |
|
198 | 205 |
render_attachment_warning_if_needed(@issue) |
199 | 206 |
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? |
200 | 207 |
app/views/issues/_list.html.erb (working copy) | ||
---|---|---|
1 | 1 |
<%= form_tag({}) do -%> |
2 | 2 |
<%= hidden_field_tag 'back_url', url_for(params), :id => nil %> |
3 | 3 |
<div class="autoscroll"> |
4 |
<table class="list issues <%= sort_css_classes %>">
|
|
4 |
<table class="list issues"> |
|
5 | 5 |
<thead> |
6 | 6 |
<tr> |
7 | 7 |
<th class="checkbox hide-when-print"> |
8 | 8 |
<%= link_to image_tag('toggle_check.png'), {}, |
9 | 9 |
:onclick => 'toggleIssuesSelection(this); return false;', |
10 |
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> |
|
11 |
</th> |
|
10 |
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
|
|
11 |
</th>
|
|
12 | 12 |
<% query.inline_columns.each do |column| %> |
13 |
<%= column_header(column) %> |
|
14 |
<% end %> |
|
13 |
<%= column_header(column) %>
|
|
14 |
<% end %>
|
|
15 | 15 |
</tr> |
16 | 16 |
</thead> |
17 |
<% previous_group, first = false, true %>
|
|
17 |
<% previous_group = false %>
|
|
18 | 18 |
<tbody> |
19 | 19 |
<% issue_list(issues) do |issue, level| -%> |
20 |
<% if @query.grouped? && ((group = @query.group_by_column.value(issue)) != previous_group || first) %>
|
|
20 |
<% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
|
|
21 | 21 |
<% reset_cycle %> |
22 | 22 |
<tr class="group open"> |
23 | 23 |
<td colspan="<%= query.inline_columns.size + 2 %>"> |
24 | 24 |
<span class="expander" onclick="toggleRowGroup(this);"> </span> |
25 |
<%= (group.blank? && group != false) ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span> |
|
26 |
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", |
|
27 |
"toggleAllRowGroups(this)", :class => 'toggle-all') %> |
|
25 |
<%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %>, Est Done: <%= (@issue_progress_by_group[group] * 100).round / 100.0 %> |
|
26 |
<% if @issue_sum_by_group[group] > 0 %> |
|
27 |
(<%= (100 * @issue_progress_by_group[group] / @issue_sum_by_group[group]).round(2) %>%), |
|
28 |
<% else %> |
|
29 |
(0.0%), |
|
30 |
<% end %> |
|
31 |
<%= l(:label_total) %>: <%= @issue_sum_by_group[group] %>)</span> |
|
32 |
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> |
|
28 | 33 |
</td> |
29 | 34 |
</tr> |
30 |
<% previous_group, first = group, false %>
|
|
35 |
<% previous_group = group %>
|
|
31 | 36 |
<% end %> |
32 | 37 |
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> |
33 | 38 |
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> |
... | ... | |
45 | 50 |
</table> |
46 | 51 |
</div> |
47 | 52 |
<% end -%> |
53 |
<p align="right"> |
|
54 |
Current page: <b><%=estimated_hours(issues) %></b> |
|
55 |
Est Done: <b><%= estimated_done(@all_issues) %> <% if estimated_hours(@all_issues) > 0 %> |
|
56 |
(<%= "#{estimated_done_percentage(@all_issues)}%" %>) |
|
57 |
<% else %>(0.0%) |
|
58 |
<% end %></b> |
|
59 |
<%= l(:label_total) %>: <b><%=@query.issue_sum %></b> |
|
60 |
</p> |
app/views/issues/show.html.erb (working copy) | ||
---|---|---|
58 | 58 |
unless @issue.disabled_core_fields.include?('estimated_hours') |
59 | 59 |
unless @issue.estimated_hours.nil? |
60 | 60 |
rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' |
61 |
end
|
|
61 |
end
|
|
62 | 62 |
end |
63 |
unless @issue.disabled_core_fields.include?('estimated_hours') |
|
64 |
rows.right "Estimated done", l_hours(@issue.estimated_done), :class => 'estimated-hours' |
|
65 |
end |
|
63 | 66 |
if User.current.allowed_to?(:view_time_entries, @project) |
64 |
rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
|
|
67 |
rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? (link_to l_hours(@issue.total_spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-"), :class => 'spent-time'
|
|
65 | 68 |
end |
66 | 69 |
end %> |
67 | 70 |
<%= render_custom_fields_rows(@issue) %> |
config/locales/en.yml (working copy) | ||
---|---|---|
300 | 300 |
field_assignable: Issues can be assigned to this role |
301 | 301 |
field_redirect_existing_links: Redirect existing links |
302 | 302 |
field_estimated_hours: Estimated time |
303 |
field_estimated_done: Estimated done |
|
303 | 304 |
field_column_names: Columns |
304 | 305 |
field_time_entries: Log time |
305 | 306 |
field_time_zone: Time zone |