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 |