Merge pull request #7485 from opf/feature/derived-estimated-time
allow editing estimated time for parents, show derived separatelypull/7525/head
commit
5bfa246527
@ -0,0 +1,118 @@ |
||||
class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2] |
||||
class WorkPackageWithRelations < ActiveRecord::Base |
||||
self.table_name = "work_packages" |
||||
|
||||
scope :with_children, ->(*args) do |
||||
rel = "relations" |
||||
wp = "work_packages" |
||||
|
||||
query = "EXISTS (SELECT 1 FROM #{rel} WHERE #{rel}.from_id = #{wp}.id AND #{rel}.hierarchy > 0 LIMIT 1)" |
||||
|
||||
where(query) |
||||
end |
||||
end |
||||
|
||||
def change |
||||
add_column :work_packages, :derived_estimated_hours, :float |
||||
add_column :work_package_journals, :derived_estimated_hours, :float |
||||
|
||||
reversible do |change| |
||||
change.up do |
||||
WorkPackage.transaction do |
||||
migrate_to_derived_estimated_hours! |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
# Before this migration all work packages who have children had their |
||||
# estimated hours set based on their children through the UpdateAncestorsService. |
||||
# |
||||
# We move this value to the derived_estimated_hours column and clear |
||||
# the estimated_hours column. In the future users can estimte the time |
||||
# for parent work pacages separately there while the UpdateAncestorsService |
||||
# only touches the derived_estimated_hours column. |
||||
def migrate_to_derived_estimated_hours! |
||||
last_id = Journal.order(id: :desc).limit(1).pluck(:id).first || 0 |
||||
wp_journals = "work_package_journals" |
||||
work_packages = WorkPackageWithRelations.with_children |
||||
|
||||
work_packages.update_all("derived_estimated_hours = estimated_hours, estimated_hours = NULL") |
||||
|
||||
create_journals_for work_packages |
||||
create_work_package_journals last_id: last_id |
||||
end |
||||
|
||||
## |
||||
# Creates a new journal for each work package with the next version. |
||||
# The respective work_package journal is created in a separate step. |
||||
def create_journals_for(work_packages, author: journal_author, notes: journal_notes) |
||||
WorkPackage.connection.execute(" |
||||
INSERT INTO #{Journal.table_name} (journable_type, journable_id, user_id, notes, created_at, version, activity_type) |
||||
SELECT |
||||
'WorkPackage', |
||||
parents.id, |
||||
#{author.id}, |
||||
#{WorkPackage.connection.quote(notes)}, |
||||
NOW(), |
||||
(SELECT MAX(version) FROM journals WHERE journable_id = parents.id AND journable_type = 'WorkPackage') + 1, |
||||
'work_packages' |
||||
FROM ( |
||||
#{work_packages.select(:id).to_sql} |
||||
) AS parents |
||||
") |
||||
end |
||||
|
||||
def journal_author |
||||
@journal_author ||= User.system |
||||
end |
||||
|
||||
def journal_notes |
||||
"_'Estimated hours' changed to 'Derived estimated hours'_" |
||||
end |
||||
|
||||
## |
||||
# Creates work package journals for the move of estimated_hours to derived_estimated_hours. |
||||
# |
||||
# For each newly created journal (see above) it inserts the respective work package's |
||||
# current estimated_hours (deleted) and derived estimated hours (previously estimated hours). |
||||
# All other attributes of the work package journal entry are copied from the previous |
||||
# work package journal entry (i.e. the values are not changed). |
||||
# |
||||
# @param last_id [Integer] The ID of the last journal before the journals for the migration were created. |
||||
def create_work_package_journals(last_id:) |
||||
journals = "journals" |
||||
wp_journals = "work_package_journals" |
||||
work_packages = "work_packages" |
||||
|
||||
WorkPackage.connection.execute(" |
||||
INSERT INTO #{wp_journals} ( |
||||
journal_id, type_id, project_id, subject, description, due_date, category_id, status_id, |
||||
assigned_to_id, priority_id, fixed_version_id, author_id, done_ratio, |
||||
start_date, parent_id, responsible_id, cost_object_id, story_points, remaining_hours, |
||||
estimated_hours, derived_estimated_hours |
||||
) |
||||
SELECT * |
||||
FROM ( |
||||
SELECT |
||||
#{journals}.id, #{wp_journals}.type_id, #{wp_journals}.project_id, #{wp_journals}.subject, |
||||
#{wp_journals}.description, #{wp_journals}.due_date, #{wp_journals}.category_id, #{wp_journals}.status_id, |
||||
#{wp_journals}.assigned_to_id, #{wp_journals}.priority_id, #{wp_journals}.fixed_version_id, #{wp_journals}.author_id, |
||||
#{wp_journals}.done_ratio, #{wp_journals}.start_date, #{wp_journals}.parent_id, #{wp_journals}.responsible_id, |
||||
#{wp_journals}.cost_object_id, #{wp_journals}.story_points, #{wp_journals}.remaining_hours, |
||||
#{work_packages}.estimated_hours, #{work_packages}.derived_estimated_hours |
||||
FROM #{journals} -- take the journal ID from here (ID of newly created journals from above) |
||||
LEFT JOIN #{work_packages} -- take the current (derived) estimated hours from here |
||||
ON #{work_packages}.id = #{journals}.journable_id AND #{journals}.journable_type = 'WorkPackage' |
||||
LEFT JOIN #{wp_journals} -- keep everything else the same |
||||
ON #{wp_journals}.journal_id = ( |
||||
SELECT MAX(id) |
||||
FROM #{journals} |
||||
WHERE journable_id = #{work_packages}.id AND journable_type = 'WorkPackage' AND #{journals}.id <= #{last_id} |
||||
-- we are selecting the latest previous (hence <= last_id) work package journal here to copy its values |
||||
) |
||||
WHERE #{journals}.id > #{last_id} -- make sure to only create entries for the newly created journals |
||||
) AS results |
||||
") |
||||
end |
||||
end |
@ -0,0 +1,168 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
RSpec.feature 'Estimated hours display' do |
||||
let(:user) { FactoryBot.create :admin } |
||||
let(:project) { FactoryBot.create :project } |
||||
|
||||
let(:hierarchy) { [] } |
||||
|
||||
let!(:work_packages) do |
||||
build_work_package_hierarchy( |
||||
hierarchy, |
||||
:subject, |
||||
:estimated_hours, |
||||
shared_attributes: { |
||||
project: project |
||||
} |
||||
) |
||||
end |
||||
|
||||
let(:parent) { work_packages.first } |
||||
let(:child) { work_packages.last } |
||||
|
||||
let!(:query) do |
||||
query = FactoryBot.build :query, user: user, project: project |
||||
query.column_names = %w[id subject estimated_hours] |
||||
|
||||
query.save! |
||||
query |
||||
end |
||||
|
||||
let(:wp_table) { Pages::WorkPackagesTable.new project } |
||||
|
||||
before do |
||||
WorkPackages::UpdateAncestorsService |
||||
.new(user: user, work_package: child) |
||||
.call([:estimated_hours]) |
||||
|
||||
login_as(user) |
||||
end |
||||
|
||||
context "with both estimated and derived estimated time" do |
||||
let(:hierarchy) do |
||||
[ |
||||
{ |
||||
["Parent", 1] => [ |
||||
["Child", 3] |
||||
] |
||||
} |
||||
] |
||||
end |
||||
|
||||
scenario 'work package index', js: true do |
||||
wp_table.visit_query query |
||||
wp_table.expect_work_package_listed parent, child |
||||
|
||||
expect(page).to have_content("Parent\n1 h(+3 h)") |
||||
end |
||||
|
||||
scenario 'work package details', js: true do |
||||
visit work_package_path(parent.id) |
||||
|
||||
expect(page).to have_content("Estimated time\n1 h(+3 h)") |
||||
end |
||||
end |
||||
|
||||
context "with just estimated time" do |
||||
let(:hierarchy) do |
||||
[ |
||||
{ |
||||
["Parent", 1] => [ |
||||
["Child", 0] |
||||
] |
||||
} |
||||
] |
||||
end |
||||
|
||||
scenario 'work package index', js: true do |
||||
wp_table.visit_query query |
||||
wp_table.expect_work_package_listed parent, child |
||||
|
||||
expect(page).to have_content("Parent\n1 h") |
||||
end |
||||
|
||||
scenario 'work package details', js: true do |
||||
visit work_package_path(parent.id) |
||||
|
||||
expect(page).to have_content("Estimated time\n1 h") |
||||
end |
||||
end |
||||
|
||||
context "with just derived estimated time" do |
||||
let(:hierarchy) do |
||||
[ |
||||
{ |
||||
["Parent", 0] => [ |
||||
["Child", 3] |
||||
] |
||||
} |
||||
] |
||||
end |
||||
|
||||
scenario 'work package index', js: true do |
||||
wp_table.visit_query query |
||||
wp_table.expect_work_package_listed parent, child |
||||
|
||||
expect(page).to have_content("Parent\n(3 h)") |
||||
end |
||||
|
||||
scenario 'work package details', js: true do |
||||
visit work_package_path(parent.id) |
||||
|
||||
expect(page).to have_content("Estimated time\n(3 h)") |
||||
end |
||||
end |
||||
|
||||
context "with neither estimated nor derived estimated time" do |
||||
let(:hierarchy) do |
||||
[ |
||||
{ |
||||
["Parent", 0] => [ |
||||
["Child", 0] |
||||
] |
||||
} |
||||
] |
||||
end |
||||
|
||||
scenario 'work package index', js: true do |
||||
wp_table.visit_query query |
||||
wp_table.expect_work_package_listed parent, child |
||||
|
||||
expect(page).to have_content("Parent\n-") |
||||
end |
||||
|
||||
scenario 'work package details', js: true do |
||||
visit work_package_path(parent.id) |
||||
|
||||
expect(page).to have_content("Estimated time\n-") |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue