Index: app/helpers/issues_helper.rb =================================================================== --- app/helpers/issues_helper.rb (revision 14229) +++ app/helpers/issues_helper.rb (working copy) @@ -427,4 +427,16 @@ end end end + + def estimated_done(issues) + issues.map(&:estimated_done).reject{|x|x.nil?}.sum.round(2) + end + + def estimated_hours(issues) + issues.map(&:estimated_hours).reject{|x| x.nil?}.sum + end + + def estimated_done_percentage(issues) + (100 * estimated_done(issues) / estimated_hours(issues)).round(2) + end end Index: app/helpers/queries_helper.rb =================================================================== --- app/helpers/queries_helper.rb (revision 14229) +++ app/helpers/queries_helper.rb (working copy) @@ -18,8 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module QueriesHelper - include ApplicationHelper - def filters_options_for_select(query) options_for_select(filters_options(query)) end @@ -83,7 +81,7 @@ end def column_content(column, issue) - value = column.value_object(issue) + value = column.value(issue) if value.is_a?(Array) value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe else @@ -97,48 +95,51 @@ link_to value, issue_path(issue) when :subject link_to value, issue_path(issue) - when :parent - value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : '' when :description issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' when :done_ratio progress_bar(value, :width => '80px') + when :estimated_done + if (value.nil?) + value = 0 + end + sprintf "%.2f", value when :relations other = value.other_issue(issue) content_tag('span', (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe, :class => value.css_classes_for(issue)) - else + else format_object(value) - end + end end def csv_content(column, issue) - value = column.value_object(issue) + value = column.value(issue) if value.is_a?(Array) value.collect {|v| csv_value(column, issue, v)}.compact.join(', ') - else + else csv_value(column, issue, value) - end + end end - def csv_value(column, object, value) - format_object(value, false) do |value| - case value.class.name - when 'Float' - sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator)) - when 'IssueRelation' - other = value.other_issue(object) - l(value.label_for(object)) + " ##{other.id}" - when 'Issue' - if object.is_a?(TimeEntry) - "#{value.tracker} ##{value.id}: #{value.subject}" - else - value.id - end - else - value - end + def csv_value(column, issue, value) + case value.class.name + when 'Time' + format_time(value) + when 'Date' + format_date(value) + when 'Float' + sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator)) + when 'IssueRelation' + other = value.other_issue(issue) + l(value.label_for(issue)) + " ##{other.id}" + when 'TrueClass' + l(:general_text_Yes) + when 'FalseClass' + l(:general_text_No) + else + value.to_s end end Index: app/models/query.rb =================================================================== --- app/models/query.rb (revision 14229) +++ app/models/query.rb (working copy) @@ -57,10 +57,6 @@ object.send name end - def value_object(object) - object.send name - end - def css_classes name end @@ -84,24 +80,13 @@ @cf end - def value_object(object) + def value(object) if custom_field.visible_by?(object.project, User.current) - cv = object.custom_values.select {|v| v.custom_field_id == @cf.id} - cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first + cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} + cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first else nil - end end - - def value(object) - raw = value_object(object) - if raw.is_a?(Array) - raw.map {|r| @cf.cast_value(r.value)} - elsif raw - @cf.cast_value(raw.value) - else - nil - end end def css_classes @@ -120,7 +105,7 @@ @association = association end - def value_object(object) + def value(object) if assoc = object.send(@association) super(assoc) end @@ -159,38 +144,38 @@ after_save do |query| if query.visibility_changed? && query.visibility != VISIBILITY_ROLES - query.roles.clear - end + query.roles.clear + end end class_attribute :operators self.operators = { "=" => :label_equals, - "!" => :label_not_equals, - "o" => :label_open_issues, - "c" => :label_closed_issues, - "!*" => :label_none, + "!" => :label_not_equals, + "o" => :label_open_issues, + "c" => :label_closed_issues, + "!*" => :label_none, "*" => :label_any, - ">=" => :label_greater_or_equal, - "<=" => :label_less_or_equal, - "><" => :label_between, - " :label_in_less_than, - ">t+" => :label_in_more_than, + ">=" => :label_greater_or_equal, + "<=" => :label_less_or_equal, + "><" => :label_between, + " :label_in_less_than, + ">t+" => :label_in_more_than, "> :label_in_the_next_days, - "t+" => :label_in, - "t" => :label_today, + "t+" => :label_in, + "t" => :label_today, "ld" => :label_yesterday, - "w" => :label_this_week, + "w" => :label_this_week, "lw" => :label_last_week, "l2w" => [:label_last_n_weeks, {:count => 2}], "m" => :label_this_month, "lm" => :label_last_month, "y" => :label_this_year, - ">t-" => :label_less_than_ago, - " :label_more_than_ago, + ">t-" => :label_less_than_ago, + " :label_more_than_ago, "> :label_in_the_past_days, - "t-" => :label_ago, - "~" => :label_contains, + "t-" => :label_ago, + "~" => :label_contains, "!~" => :label_not_contains, "=p" => :label_any_issues_in_project, "=!p" => :label_any_issues_not_in_project, @@ -200,14 +185,14 @@ class_attribute :operators_by_filter_type self.operators_by_filter_type = { :list => [ "=", "!" ], - :list_status => [ "o", "=", "!", "c", "*" ], - :list_optional => [ "=", "!", "!*", "*" ], - :list_subprojects => [ "*", "!*", "=" ], + :list_status => [ "o", "=", "!", "c", "*" ], + :list_optional => [ "=", "!", "!*", "*" ], + :list_subprojects => [ "*", "!*", "=" ], :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~", "!*", "*" ], - :text => [ "~", "!~", "!*", "*" ], - :integer => [ "=", ">=", "<=", "><", "!*", "*" ], + :string => [ "=", "~", "!", "!~", "!*", "*" ], + :text => [ "~", "!~", "!*", "*" ], + :integer => [ "=", ">=", "<=", "><", "!*", "*" ], :float => [ "=", ">=", "<=", "><", "!*", "*" ], :relation => ["=", "=p", "=!p", "!p", "!*", "*"] } @@ -301,29 +286,29 @@ json = {} available_filters.each do |field, options| json[field] = options.slice(:type, :name, :values).stringify_keys - end + end json - end + end def all_projects @all_projects ||= Project.visible.all - end + end def all_projects_values return @all_projects_values if @all_projects_values values = [] - Project.project_tree(all_projects) do |p, level| - prefix = (level > 0 ? ('--' * level + ' ') : '') + Project.project_tree(all_projects) do |p, level| + prefix = (level > 0 ? ('--' * level + ' ') : '') values << ["#{prefix}#{p.name}", p.id.to_s] - end + end @all_projects_values = values - end + end # Adds available filters def initialize_available_filters # implemented by sub-classes - end + end protected :initialize_available_filters # Adds an available filter @@ -331,14 +316,14 @@ @available_filters ||= ActiveSupport::OrderedHash.new @available_filters[field] = options @available_filters - end + end # Removes an available filter def delete_available_filter(field) if @available_filters @available_filters.delete(field) - end - end + end + end # Return a hash of available filters def available_filters @@ -417,8 +402,8 @@ # Returns a Hash of columns and the key for sorting def sortable_columns available_columns.inject({}) {|h, column| - h[column.name.to_s] = column.sortable - h + h[column.name.to_s] = column.sortable + h } end @@ -436,7 +421,7 @@ def block_columns columns.reject(&:inline?) - end + end def available_inline_columns available_columns.select(&:inline?) @@ -556,7 +541,7 @@ next unless v and !v.empty? operator = operator_for(field) - # "me" value substitution + # "me" value subsitution if %w(assigned_to_id author_id user_id watcher_id).include?(field) if v.delete("me") if User.current.logged? @@ -622,7 +607,7 @@ customized_key = "#{assoc}_id" customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class - end + end where = sql_for_field(field, operator, value, db_table, db_field, true) if operator =~ /[<>]/ where = "(#{where}) AND #{db_table}.#{db_field} <> ''" @@ -793,12 +778,12 @@ if assoc.present? filter_id = "#{assoc}.#{filter_id}" filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) - end + end add_available_filter filter_id, options.merge({ :name => filter_name, :field => field }) - end + end # Adds filters for the given custom fields scope def add_custom_fields_filters(scope, assoc=nil) Index: app/models/issue_query.rb =================================================================== --- app/models/issue_query.rb (revision 14229) +++ app/models/issue_query.rb (working copy) @@ -38,6 +38,7 @@ QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), + QueryColumn.new(:estimated_done, :sortable => "#{Issue.table_name}.estimated_done", :caption => :field_estimated_done), QueryColumn.new(:relations, :caption => :label_related_issues), QueryColumn.new(:description, :inline => false) ] @@ -147,7 +148,6 @@ end principals.uniq! principals.sort! - principals.reject! {|p| p.is_a?(GroupBuiltin)} users = principals.select {|p| p.is_a?(User)} add_available_filter "status_id", @@ -184,7 +184,7 @@ :type => :list_optional, :values => assigned_to_values ) unless assigned_to_values.empty? - group_values = Group.givable.collect {|g| [g.name, g.id.to_s] } + group_values = Group.all.collect {|g| [g.name, g.id.to_s] } add_available_filter("member_of_group", :type => :list_optional, :values => group_values ) unless group_values.empty? @@ -214,6 +214,7 @@ add_available_filter "due_date", :type => :date add_available_filter "estimated_hours", :type => :float add_available_filter "done_ratio", :type => :integer + add_available_filter "estimated_done", :type => :float if User.current.allowed_to?(:set_issues_private, nil, :global => true) || User.current.allowed_to?(:set_own_issues_private, nil, :global => true) @@ -293,7 +294,21 @@ rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end + + # Returns sum of all the issue's estimated_hours + def issue_sum + Issue.visible.sum(:estimated_hours, :include => [:status, :project], :conditions => statement) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns sum of all the issue's estimated_done + def issue_sum_in_progress + Issue.visible.sum(:estimated_done, :include => [:status, :project], :conditions => statement) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns the issue count by group or nil if query is not grouped def issue_count_by_group r = nil @@ -318,7 +333,47 @@ rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end - + + # Returns sum of the issue's estimated_hours by group or nil if query is not grouped + def issue_sum_by_group + r = nil + if grouped? + begin + r = Issue.visible.sum(:estimated_hours, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) + rescue ActiveRecord::RecordNotFound + r= {r => issue_sum} + end + + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h,k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns sum of the issue's estimated_done by group or nil if query is not grouped + def issue_progress_by_group + r = nil + if grouped? + begin + r = Issue.visible.sum(:estimated_done, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) + rescue ActiveRecord::RecordNotFound + r= {r => issue_sum_by_group} + end + + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns the issues # Valid options are :order, :offset, :limit, :include, :conditions def issues(options={}) @@ -405,10 +460,10 @@ def sql_for_member_of_group_field(field, operator, value) if operator == '*' # Any group - groups = Group.givable + groups = Group.all operator = '=' # Override the operator since we want to find by assigned_to elsif operator == "!*" - groups = Group.givable + groups = Group.all operator = '!' # Override the operator since we want to find by assigned_to else groups = Group.where(:id => value).all Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 14229) +++ app/models/issue.rb (working copy) @@ -33,7 +33,7 @@ has_many :visible_journals, :class_name => 'Journal', :as => :journalized, - :conditions => Proc.new { + :conditions => Proc.new { ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false] }, :readonly => true @@ -94,7 +94,7 @@ before_create :default_assign before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on, :set_assigned_to_was - after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} + after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal # Should be after_create but would be called before previous after_save callbacks @@ -122,9 +122,9 @@ end else "(#{table_name}.is_private = #{connection.quoted_false})" - end end end + end # Returns true if usr or current user is allowed to view the issue def visible?(usr=nil) @@ -142,9 +142,9 @@ end else !self.is_private? - end end end + end # Returns true if user or current user is allowed to edit or add a note to the issue def editable?(user=User.current) @@ -195,7 +195,6 @@ @workflow_rule_by_attribute = nil @assignable_versions = nil @relations = nil - @spent_hours = nil base_reload(*args) end @@ -219,7 +218,7 @@ self.status = issue.status self.author = User.current unless options[:attachments] == false - self.attachments = issue.attachments.map do |attachement| + self.attachments = issue.attachments.map do |attachement| attachement.copy(:container => self) end end @@ -356,6 +355,10 @@ write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end + def estimated_done=(h) + write_attribute :estimated_done, (h.is_a?(String) ? h.to_hours : h) + end + safe_attributes 'project_id', :if => lambda {|issue, user| if issue.new_record? @@ -395,10 +398,10 @@ :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} safe_attributes 'private_notes', - :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} + :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} safe_attributes 'watcher_user_ids', - :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} + :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} safe_attributes 'is_private', :if => lambda {|issue, user| @@ -454,8 +457,8 @@ s = attrs['parent_issue_id'].to_s unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) @invalid_parent_issue_id = attrs.delete('parent_issue_id') - end end + end if attrs['custom_field_values'].present? editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s} @@ -530,7 +533,7 @@ return {} if roles.empty? result = {} - workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)) + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all if workflow_permissions.any? workflow_rules = workflow_permissions.inject({}) do |h, wp| h[wp.field_name] ||= {} @@ -569,7 +572,7 @@ private :workflow_rule_by_attribute def done_ratio - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0 status.default_done_ratio else read_attribute(:done_ratio) @@ -641,7 +644,7 @@ errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') end else - if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute) + if respond_to?(attribute) && send(attribute).blank? errors.add attribute, :blank end end @@ -651,7 +654,7 @@ # Set the done_ratio using the status if that setting is set. This will keep the done_ratios # even if the user turns off the setting later def update_done_ratio_from_issue_status - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0 self.done_ratio = status.default_done_ratio end end @@ -757,7 +760,7 @@ elsif project_id_changed? if project.shared_versions.include?(fixed_version) versions << fixed_version - end + end else versions << fixed_version end @@ -770,6 +773,15 @@ !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? end + def update_estimated_done + if children.count < 1 + x1 = Issue.find_by_id(id).done_ratio.to_f + x2 = Issue.find_by_id(id).estimated_hours.to_f + r = ((x1 * x2)/100).round(2) + Issue.update(id, :estimated_done => r) + end + end + # Returns an array of statuses that user is able to apply def new_statuses_allowed_to(user=User.current, include_default=false) if new_record? && @copied_from @@ -784,7 +796,7 @@ initial_status ||= status initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id - assignee_transitions_allowed = initial_assigned_to_id.present? && + assignee_transitions_allowed = initial_assigned_to_id.present? && (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id)) statuses = initial_status.find_new_statuses_allowed_to( @@ -977,7 +989,7 @@ elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY) queue << child issue_status[child] = ePROCESS_ALL - end + end end end @@ -1080,13 +1092,13 @@ # or if it starts before the given date if start_date == leaf.start_date || date > leaf.start_date leaf.reschedule_on!(date) - end + end else leaf.reschedule_on!(date) - end - end end end + end + end def <=>(issue) if issue.nil? @@ -1129,15 +1141,13 @@ def self.update_versions_from_hierarchy_change(project) moved_project_ids = project.self_and_descendants.reload.collect(&:id) # Update issues of the moved projects and issues assigned to a version of a moved project - Issue.update_versions( - ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", - moved_project_ids, moved_project_ids] - ) + Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) end def parent_issue_id=(arg) s = arg.to_s.strip.presence if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) + @parent_issue.id @invalid_parent_issue_id = nil elsif s.blank? @parent_issue = nil @@ -1177,7 +1187,6 @@ end end - # Returns an issue scope based on project and scope def self.cross_project_scope(project, scope=nil) if project.nil? return Issue @@ -1199,6 +1208,7 @@ end end + # Extracted from the ReportsController. def self.by_tracker(project) count_and_group_by(:project => project, @@ -1237,13 +1247,13 @@ end def self.by_subproject(project) - ActiveRecord::Base.connection.select_all("select s.id as status_id, - s.is_closed as closed, + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, #{Issue.table_name}.project_id as project_id, - count(#{Issue.table_name}.id) as total - from + count(#{Issue.table_name}.id) as total + from #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s - where + where #{Issue.table_name}.status_id=s.id and #{Issue.table_name}.project_id = #{Project.table_name}.id and #{visible_condition(User.current, :project => project, :with_subprojects => true)} @@ -1333,7 +1343,9 @@ if root_id.nil? # issue was just created self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) - Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id]) + set_default_left_and_right + Issue.where(["id = ?", id]). + update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt]) if @parent_issue move_to_child_of(@parent_issue) end @@ -1356,16 +1368,13 @@ move_to_right_of(root) end old_root_id = root_id - in_tenacious_transaction do - @parent_issue.reload_nested_set if @parent_issue - self.reload_nested_set - self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) - cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt] - self.class.base_class.select('id').lock(true).where(cond) - offset = right_most_bound + 1 - lft - Issue.where(cond). - update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) - end + self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id ) + target_maxright = nested_set_scope.maximum(right_column_name) || 0 + offset = target_maxright + 1 - lft + Issue.where(["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]). + update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) + self[left_column_name] = lft + offset + self[right_column_name] = rgt + offset if @parent_issue move_to_child_of(@parent_issue) end @@ -1397,7 +1406,7 @@ if p.start_date && p.due_date && p.due_date < p.start_date p.start_date, p.due_date = p.due_date, p.start_date end - + # done ratio = weighted average ratio of leaves unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio leaves_count = p.leaves.count @@ -1406,18 +1415,30 @@ if average == 0 average = 1 end - done = p.leaves.joins(:status). - sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " + - "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f - progress = done / (average * leaves_count) - p.done_ratio = progress.round + + #--original code-- + #change done_ratio to be the sum of children done_ratio + #done = p.leaves.joins(:status). + # sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " + + # "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f + #progress = done / (average * leaves_count) + #p.done_ratio = progress.round + #--end of original code--- end + end - - # estimate = sum of leaves estimates + + # estimate = sum of leaves estimates p.estimated_hours = p.leaves.sum(:estimated_hours).to_f + p.estimated_done = p.leaves.sum(:estimated_done).to_f + + if (p.estimated_hours > 0) + p.done_ratio = (100 * p.estimated_done / p.estimated_hours).to_f.round(2) + end + p.estimated_hours = nil if p.estimated_hours == 0.0 - + p.estimated_done = nil if p.estimated_done == 0.0 + # ancestors will be recursively updated p.save(:validate => false) end @@ -1477,7 +1498,7 @@ def close_duplicates if closing? duplicates.each do |duplicate| - # Reload is needed in case the duplicate was updated by a previous duplicate + # Reload is need in case the duplicate was updated by a previous duplicate duplicate.reload # Don't re-close it if it's already closed next if duplicate.closed? @@ -1497,9 +1518,9 @@ self.updated_on = current_time_from_proper_timezone if new_record? self.created_on = updated_on - end end end + end # Callback for setting closed_on when the issue is closed. # The closed_on attribute stores the time of the last closing @@ -1532,11 +1553,11 @@ before = @custom_values_before_change[c.custom_field_id] after = c.value next if before == after || (before.blank? && after.blank?) - + if before.is_a?(Array) || after.is_a?(Array) before = [before] unless before.is_a?(Array) after = [after] unless after.is_a?(Array) - + # values removed (before - after).reject(&:blank?).each do |value| @current_journal.details << JournalDetail.new(:property => 'cf', @@ -1597,17 +1618,19 @@ where = "#{Issue.table_name}.#{select_field}=j.id" - ActiveRecord::Base.connection.select_all("select s.id as status_id, - s.is_closed as closed, + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, j.id as #{select_field}, - count(#{Issue.table_name}.id) as total - from + count(#{Issue.table_name}.id) as total + from #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j - where - #{Issue.table_name}.status_id=s.id + where + #{Issue.table_name}.status_id=s.id and #{where} and #{Issue.table_name}.project_id=#{Project.table_name}.id and #{visible_condition(User.current, :project => project)} group by s.id, s.is_closed, j.id") end end + + Index: app/controllers/issues_controller.rb =================================================================== --- app/controllers/issues_controller.rb (revision 14229) +++ app/controllers/issues_controller.rb (working copy) @@ -81,7 +81,12 @@ :order => sort_clause, :offset => @offset, :limit => @limit) + + @all_issues = @query.issues(:include => [:status, :project, :assigned_to, :tracker, :priority, :category, :fixed_version]) + @issue_count_by_group = @query.issue_count_by_group + @issue_sum_by_group = @query.issue_sum_by_group + @issue_progress_by_group = @query.issue_progress_by_group respond_to do |format| format.html { render :template => 'issues/index', :layout => !request.xhr? } @@ -148,6 +153,7 @@ call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) if @issue.save + @issue.update_estimated_done call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) respond_to do |format| format.html { @@ -195,6 +201,7 @@ end if saved + @issue.update_estimated_done render_attachment_warning_if_needed(@issue) flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? Index: app/views/issues/_list.html.erb =================================================================== --- app/views/issues/_list.html.erb (revision 14229) +++ app/views/issues/_list.html.erb (working copy) @@ -1,33 +1,38 @@ <%= form_tag({}) do -%> <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
- +
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <% query.inline_columns.each do |column| %> - <%= column_header(column) %> - <% end %> + <%= column_header(column) %> + <% end %> - <% previous_group, first = false, true %> + <% previous_group = false %> <% issue_list(issues) do |issue, level| -%> - <% if @query.grouped? && ((group = @query.group_by_column.value(issue)) != previous_group || first) %> + <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %> <% reset_cycle %> - <% previous_group, first = group, false %> + <% previous_group = group %> <% end %> "> @@ -45,3 +50,11 @@
<%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this); return false;', - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> -
  - <%= (group.blank? && group != false) ? l(:label_none) : column_content(@query.group_by_column, issue) %> <%= @issue_count_by_group[group] %> - <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", - "toggleAllRowGroups(this)", :class => 'toggle-all') %> + <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <%= @issue_count_by_group[group] %>, Est Done: <%= (@issue_progress_by_group[group] * 100).round / 100.0 %> + <% if @issue_sum_by_group[group] > 0 %> + (<%= (100 * @issue_progress_by_group[group] / @issue_sum_by_group[group]).round(2) %>%), + <% else %> + (0.0%), + <% end %> + <%= l(:label_total) %>: <%= @issue_sum_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
<%= check_box_tag("ids[]", issue.id, false, :id => nil) %>
<% end -%> +

+ Current page: <%=estimated_hours(issues) %> + Est Done: <%= estimated_done(@all_issues) %> <% if estimated_hours(@all_issues) > 0 %> + (<%= "#{estimated_done_percentage(@all_issues)}%" %>) + <% else %>(0.0%) + <% end %> + <%= l(:label_total) %>: <%=@query.issue_sum %> +

Index: app/views/issues/show.html.erb =================================================================== --- app/views/issues/show.html.erb (revision 14229) +++ app/views/issues/show.html.erb (working copy) @@ -58,10 +58,13 @@ unless @issue.disabled_core_fields.include?('estimated_hours') unless @issue.estimated_hours.nil? rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' - end + end end + unless @issue.disabled_core_fields.include?('estimated_hours') + rows.right "Estimated done", l_hours(@issue.estimated_done), :class => 'estimated-hours' + end if User.current.allowed_to?(:view_time_entries, @project) - 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' + 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' end end %> <%= render_custom_fields_rows(@issue) %> Index: config/locales/en.yml =================================================================== --- config/locales/en.yml (revision 14229) +++ config/locales/en.yml (working copy) @@ -300,6 +300,7 @@ field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time + field_estimated_done: Estimated done field_column_names: Columns field_time_entries: Log time field_time_zone: Time zone