mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Fix/wp sums in db (#8580)
* remove apparently unused methods * have specs for wp query sums * grouped sums in sql * sql for total sums * alter interface of all_grouped_sums Since we now fetch all sums in one sql statement it no longer makes sense to fetch the group sums individually * remove now unused method * extract method * Add material_costs to summing * add labor_costs to group sums * add overall costs to sum * fix sum grouping descision * fix summable? check * remove work_package_list_summable_columns setting Now all summable columns are always summed. The user no longer needs to select summing up in the settings. Selecting the column to be displayed and activiating sums will suffice * fix flickering spec
This commit is contained in:
@@ -30,16 +30,17 @@
|
||||
|
||||
class Queries::Columns::Base
|
||||
attr_reader :groupable,
|
||||
:sortable,
|
||||
:association
|
||||
:sortable
|
||||
|
||||
attr_accessor :name,
|
||||
:sortable_join,
|
||||
:summable,
|
||||
:null_handling,
|
||||
:default_order
|
||||
:default_order,
|
||||
:association
|
||||
|
||||
alias_method :summable?, :summable
|
||||
attr_writer :null_handling,
|
||||
:summable_select,
|
||||
:summable_work_packages_select
|
||||
|
||||
def initialize(name, options = {})
|
||||
self.name = name
|
||||
@@ -48,6 +49,8 @@ class Queries::Columns::Base
|
||||
sortable_join
|
||||
groupable
|
||||
summable
|
||||
summable_select
|
||||
summable_work_packages_select
|
||||
association
|
||||
null_handling
|
||||
default_order).each do |attribute|
|
||||
@@ -75,10 +78,6 @@ class Queries::Columns::Base
|
||||
@sortable = name_or_value_or_false(value)
|
||||
end
|
||||
|
||||
def association=(value)
|
||||
@association = value
|
||||
end
|
||||
|
||||
# Returns true if the column is sortable, otherwise false
|
||||
def sortable?
|
||||
!!sortable
|
||||
@@ -89,8 +88,28 @@ class Queries::Columns::Base
|
||||
!!groupable
|
||||
end
|
||||
|
||||
def value(issue)
|
||||
issue.send name
|
||||
def summable?
|
||||
summable || @summable_select || @summable_work_packages_select
|
||||
end
|
||||
|
||||
def summable_select
|
||||
@summable_select || name
|
||||
end
|
||||
|
||||
def summable_work_packages_select
|
||||
if @summable_work_packages_select == false
|
||||
nil
|
||||
elsif @summable_work_packages_select
|
||||
@summable_work_packages_select
|
||||
elsif summable&.respond_to?(:call)
|
||||
nil
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
def value(model)
|
||||
model.send name
|
||||
end
|
||||
|
||||
def self.instances(_context = nil)
|
||||
|
||||
@@ -32,29 +32,12 @@ class Queries::WorkPackages::Columns::CustomFieldColumn < Queries::WorkPackages:
|
||||
def initialize(custom_field)
|
||||
super
|
||||
|
||||
set_name! custom_field
|
||||
set_sortable! custom_field
|
||||
set_groupable! custom_field
|
||||
set_summable! custom_field
|
||||
|
||||
@cf = custom_field
|
||||
end
|
||||
|
||||
def set_name!(custom_field)
|
||||
self.name = "cf_#{custom_field.id}".to_sym
|
||||
end
|
||||
|
||||
def set_sortable!(custom_field)
|
||||
self.sortable = custom_field.order_statements || false
|
||||
end
|
||||
|
||||
def set_groupable!(custom_field)
|
||||
self.groupable = custom_field.group_by_statements if groupable_custom_field?(custom_field)
|
||||
self.groupable ||= false
|
||||
end
|
||||
|
||||
def set_summable!(custom_field)
|
||||
self.summable = %w(float int).include?(custom_field.field_format)
|
||||
set_name!
|
||||
set_sortable!
|
||||
set_groupable!
|
||||
set_summable!
|
||||
end
|
||||
|
||||
def groupable_custom_field?(custom_field)
|
||||
@@ -77,23 +60,6 @@ class Queries::WorkPackages::Columns::CustomFieldColumn < Queries::WorkPackages:
|
||||
work_package.formatted_custom_value_for(@cf.id)
|
||||
end
|
||||
|
||||
def sum_of(work_packages)
|
||||
if work_packages.respond_to?(:joins)
|
||||
cast = @cf.field_format == 'int' ? 'BIGINT' : 'FLOAT'
|
||||
|
||||
CustomValue
|
||||
.where(customized: work_packages, custom_field: @cf)
|
||||
.where.not(value: nil)
|
||||
.where.not(value: '')
|
||||
.pluck("SUM(value::#{cast})")
|
||||
.first
|
||||
else
|
||||
# TODO: eliminate calls of this method with an Array and drop the :compact call below
|
||||
ActiveSupport::Deprecation.warn('Passing an array of work packages is deprecated. Pass an AR-relation instead.')
|
||||
work_packages.map { |wp| wp.typed_custom_value_for(@cf) }.compact.reduce(:+)
|
||||
end
|
||||
end
|
||||
|
||||
def self.instances(context = nil)
|
||||
if context
|
||||
context.all_work_package_custom_fields
|
||||
@@ -103,4 +69,49 @@ class Queries::WorkPackages::Columns::CustomFieldColumn < Queries::WorkPackages:
|
||||
.reject { |cf| cf.field_format == 'text' }
|
||||
.map { |cf| new(cf) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_name!
|
||||
self.name = "cf_#{custom_field.id}".to_sym
|
||||
end
|
||||
|
||||
def set_sortable!
|
||||
self.sortable = custom_field.order_statements || false
|
||||
end
|
||||
|
||||
def set_groupable!
|
||||
self.groupable = custom_field.group_by_statements if groupable_custom_field?(custom_field)
|
||||
self.groupable ||= false
|
||||
end
|
||||
|
||||
def set_summable!
|
||||
self.summable = if %w(float int).include?(custom_field.field_format)
|
||||
select = summable_select_statement
|
||||
|
||||
->(query, grouped) {
|
||||
Queries::WorkPackages::Columns::WorkPackageColumn
|
||||
.scoped_column_sum(summable_scope(query), select, grouped && query.group_by_statement)
|
||||
}
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def summable_scope(query)
|
||||
WorkPackage
|
||||
.where(id: query.results.work_packages)
|
||||
.left_joins(:custom_values)
|
||||
.where(custom_values: { custom_field: custom_field })
|
||||
.where.not(custom_values: { value: nil })
|
||||
.where.not(custom_values: { value: '' })
|
||||
end
|
||||
|
||||
def summable_select_statement
|
||||
if custom_field.field_format == 'int'
|
||||
"COALESCE(SUM(value::BIGINT)::BIGINT, 0) #{name}"
|
||||
else
|
||||
"COALESCE(ROUND(SUM(value::NUMERIC), 2)::FLOAT, 0.0) #{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,13 +41,17 @@ class Queries::WorkPackages::Columns::WorkPackageColumn < Queries::Columns::Base
|
||||
WorkPackage.human_attribute_name(name)
|
||||
end
|
||||
|
||||
def sum_of(work_packages)
|
||||
if work_packages.is_a?(Array)
|
||||
# TODO: Sums::grouped_sums might call through here without an AR::Relation
|
||||
# Ensure that this also calls using a Relation and drop this (slow!) implementation
|
||||
work_packages.map { |wp| value(wp) }.compact.reduce(:+)
|
||||
def self.scoped_column_sum(scope, select, group_by)
|
||||
scope = scope
|
||||
.except(:order, :select)
|
||||
|
||||
if group_by
|
||||
scope
|
||||
.group(group_by)
|
||||
.select("#{group_by} id", select)
|
||||
else
|
||||
work_packages.sum(name)
|
||||
scope
|
||||
.select(select)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+5
-5
@@ -266,6 +266,10 @@ class Query < ApplicationRecord
|
||||
.merge(column_sortability)
|
||||
end
|
||||
|
||||
def summed_up_columns
|
||||
available_columns.select(&:summable?)
|
||||
end
|
||||
|
||||
def columns
|
||||
column_list = if has_default_columns?
|
||||
column_list = Setting.work_package_list_default_columns.dup.map(&:to_sym)
|
||||
@@ -350,11 +354,7 @@ class Query < ApplicationRecord
|
||||
end
|
||||
|
||||
def display_sums?
|
||||
display_sums && any_summable_columns?
|
||||
end
|
||||
|
||||
def any_summable_columns?
|
||||
Setting.work_package_list_summable_columns.any?
|
||||
display_sums
|
||||
end
|
||||
|
||||
def group_by_column
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
#++
|
||||
|
||||
class ::Query::Results
|
||||
include ::Query::GroupBy
|
||||
include ::Query::Sums
|
||||
include ::Query::Results::GroupBy
|
||||
include ::Query::Results::Sums
|
||||
include Redmine::I18n
|
||||
|
||||
attr_accessor :query
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
# See docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module ::Query::GroupBy
|
||||
module ::Query::Results::GroupBy
|
||||
# Returns the work package count by group or nil if query is not grouped
|
||||
def work_package_count_by_group
|
||||
@work_package_count_by_group ||= begin
|
||||
@@ -177,7 +177,7 @@ module ::Query::GroupBy
|
||||
# Retrieve the defined order for the group by
|
||||
# IF it occurs in the sort criteria
|
||||
def order_for_group_by(column)
|
||||
sort_entry = query.sort_criteria.detect { |column, _dir| column == query.group_by }
|
||||
sort_entry = query.sort_criteria.detect { |c, _dir| c == query.group_by }
|
||||
order = sort_entry&.last || column.default_order
|
||||
|
||||
"#{order} #{column.null_handling(order == 'asc')}"
|
||||
@@ -0,0 +1,128 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module ::Query::Results::Sums
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
def all_total_sums
|
||||
group_sums = sums_select
|
||||
|
||||
query.summed_up_columns.inject({}) do |result, column|
|
||||
value = group_sums.first
|
||||
result[column] = value[column.name.to_s] unless value.nil?
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def all_group_sums
|
||||
return nil unless query.grouped?
|
||||
|
||||
sums_by_id = sums_select(true).inject({}) do |result, group_sum|
|
||||
result[group_sum['id']] = {}
|
||||
|
||||
query.summed_up_columns.each do |column|
|
||||
result[group_sum['id']][column] = group_sum[column.name.to_s]
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
transform_group_keys(sums_by_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sums_select(grouped = false)
|
||||
select = if grouped
|
||||
["work_packages.id"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
select += query.summed_up_columns.map(&:summable_select)
|
||||
|
||||
sql = <<~SQL
|
||||
SELECT #{select.join(', ')}
|
||||
FROM (#{sums_work_package_scope(grouped).to_sql}) work_packages
|
||||
#{sums_callable_joins(grouped)}
|
||||
SQL
|
||||
|
||||
connection = ActiveRecord::Base.connection
|
||||
|
||||
connection.uncached do
|
||||
connection.select_all(sql)
|
||||
end
|
||||
end
|
||||
|
||||
def sums_work_package_scope(grouped)
|
||||
scope = WorkPackage
|
||||
.where(id: work_packages)
|
||||
.except(:order, :select)
|
||||
.select(sums_work_package_scope_selects(grouped))
|
||||
|
||||
if grouped
|
||||
scope.group(query.group_by_statement)
|
||||
else
|
||||
scope
|
||||
end
|
||||
end
|
||||
|
||||
def sums_callable_joins(grouped)
|
||||
callable_summed_up_columns
|
||||
.map do |c|
|
||||
join_condition = if grouped
|
||||
"#{c.name}.id = work_packages.id OR #{c.name}.id IS NULL AND work_packages.id IS NULL"
|
||||
else
|
||||
"TRUE"
|
||||
end
|
||||
|
||||
"LEFT OUTER JOIN (#{c.summable.(query, grouped).to_sql}) #{c.name} ON #{join_condition}"
|
||||
end
|
||||
.join(' ')
|
||||
end
|
||||
|
||||
def sums_work_package_scope_selects(grouped)
|
||||
select = if grouped
|
||||
["#{query.group_by_statement} id"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
select + query.summed_up_columns.map(&:summable_work_packages_select).compact.map { |c| "SUM(#{c}) #{c}" }
|
||||
end
|
||||
|
||||
def callable_summed_up_columns
|
||||
query.summed_up_columns.select { |column| column.summable.respond_to?(:call) }
|
||||
end
|
||||
|
||||
def non_callable_summed_up_columns
|
||||
query.summed_up_columns.map { |column| column.summable.respond_to?(:call) }
|
||||
end
|
||||
end
|
||||
@@ -1,147 +0,0 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module ::Query::Sums
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
def next_in_same_group?(issue = cached_issue)
|
||||
caching_issue issue do |issue|
|
||||
!last_issue? &&
|
||||
query.group_by_column.value(issue) == query.group_by_column.value(work_packages[issue_index + 1])
|
||||
end
|
||||
end
|
||||
|
||||
def last_issue?(issue = cached_issue)
|
||||
caching_issue issue do |_issue|
|
||||
issue_index == work_packages.size - 1
|
||||
end
|
||||
end
|
||||
|
||||
def issue_index(issue = cached_issue)
|
||||
caching_issue issue do |issue|
|
||||
work_packages.find_index(issue)
|
||||
end
|
||||
end
|
||||
|
||||
def grouped_sum_of_issue(column, issue = cached_issue)
|
||||
grouped_sum_of column, group_for_issue(issue)
|
||||
end
|
||||
|
||||
def grouped_sum_of(column, group)
|
||||
sum_of column, group
|
||||
end
|
||||
|
||||
def grouped_sums(column)
|
||||
work_packages
|
||||
.map { |wp| query.group_by_column.value(wp) }
|
||||
.uniq
|
||||
.inject({}) do |group_sums, current_group|
|
||||
work_packages_in_current_group = work_packages.select { |wp| query.group_by_column.value(wp) == current_group }
|
||||
# TODO: sum_of only works fast when passing an AR::Relation
|
||||
group_sums.merge current_group => sum_of(column, work_packages_in_current_group)
|
||||
end
|
||||
end
|
||||
|
||||
def total_sum_of(column)
|
||||
sum_of(column, work_packages)
|
||||
end
|
||||
|
||||
def sum_of(column, collection)
|
||||
return nil unless should_be_summed_up?(column)
|
||||
|
||||
sum = column.sum_of(collection)
|
||||
|
||||
crunch(sum)
|
||||
end
|
||||
|
||||
def caching_issue(issue)
|
||||
@cached_issue = issue unless @cached_issue == issue
|
||||
block_given? ? yield(issue) : issue
|
||||
end
|
||||
|
||||
def cached_issue
|
||||
@cached_issue
|
||||
end
|
||||
|
||||
def mapping_for(column)
|
||||
if column.respond_to? :real_value
|
||||
method(:number_to_currency)
|
||||
else
|
||||
# respond_to? :call, but do nothing
|
||||
@nilproc ||= Proc.new { |val| val }
|
||||
end
|
||||
end
|
||||
|
||||
def crunch(num)
|
||||
return num if num.nil? || !num.respond_to?(:integer?) || num.integer?
|
||||
|
||||
Float(format('%.2f', num.to_f))
|
||||
end
|
||||
|
||||
def group_for_issue(issue = @current_issue)
|
||||
caching_issue issue do |issue|
|
||||
work_packages.select do |is|
|
||||
query.group_by_column.value(issue) == query.group_by_column.value(is)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def should_be_summed_up?(column)
|
||||
column.summable? && Setting.work_package_list_summable_columns.include?(column.name.to_s)
|
||||
end
|
||||
|
||||
def column_total_sums
|
||||
query.columns.map { |column| total_sum_of(column) }
|
||||
end
|
||||
|
||||
def all_total_sums
|
||||
query.available_columns.select { |column|
|
||||
should_be_summed_up?(column)
|
||||
}.inject({}) { |result, column|
|
||||
sum = total_sum_of(column)
|
||||
result[column] = sum unless sum.nil?
|
||||
result
|
||||
}
|
||||
end
|
||||
|
||||
def all_sums_for_group(group)
|
||||
return nil unless query.grouped?
|
||||
|
||||
group_work_packages = work_packages.select { |wp| query.group_by_column.value(wp) == group }
|
||||
query.available_columns.inject({}) do |result, column|
|
||||
sum = sum_of(column, group_work_packages)
|
||||
result[column] = sum unless sum.nil?
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def column_group_sums
|
||||
query.group_by_column && query.columns.map { |column| grouped_sums(column) }
|
||||
end
|
||||
end
|
||||
@@ -50,13 +50,11 @@ class WorkPackageCustomField < CustomField
|
||||
}
|
||||
|
||||
def self.summable
|
||||
ids = Setting.work_package_list_summable_columns.map do |column_name|
|
||||
if match = /cf_(\d+)/.match(column_name)
|
||||
match[1]
|
||||
end
|
||||
end.compact
|
||||
where(field_format: %w[int float])
|
||||
end
|
||||
|
||||
where(id: ids)
|
||||
def summable?
|
||||
%w[int float].include?(field_format)
|
||||
end
|
||||
|
||||
def type_name
|
||||
|
||||
@@ -99,13 +99,10 @@ module API
|
||||
return unless query.grouped?
|
||||
|
||||
results = query.results
|
||||
sums = generate_group_sums
|
||||
|
||||
results.work_package_count_by_group.map do |group, count|
|
||||
sums = if query.display_sums?
|
||||
format_query_sums results.all_sums_for_group(group)
|
||||
end
|
||||
|
||||
::API::Decorators::AggregationGroup.new(group, count, query: results.query, sums: sums, current_user: current_user)
|
||||
::API::Decorators::AggregationGroup.new(group, count, query: query, sums: sums[group], current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,6 +112,14 @@ module API
|
||||
format_query_sums query.results.all_total_sums
|
||||
end
|
||||
|
||||
def generate_group_sums
|
||||
return {} unless query.display_sums?
|
||||
|
||||
query.results.all_group_sums.transform_values do |v|
|
||||
format_query_sums(v)
|
||||
end
|
||||
end
|
||||
|
||||
def format_query_sums(sums)
|
||||
OpenStruct.new(format_column_keys(sums).merge(available_custom_fields: WorkPackageCustomField.summable.to_a))
|
||||
end
|
||||
|
||||
@@ -72,12 +72,10 @@ See docs/COPYRIGHT.rdoc for more details.
|
||||
<fieldset class="form--fieldset"><legend class="form--fieldset-legend"><%= t(:setting_column_options) %></legend>
|
||||
<%
|
||||
column_choices = Query.new.available_columns.map { |column|
|
||||
choice = { caption: column.caption, value: column.name.to_s }
|
||||
choice[:except] = :work_package_list_summable_columns unless column.summable?
|
||||
choice
|
||||
{ caption: column.caption, value: column.name.to_s }
|
||||
}
|
||||
%>
|
||||
<%= settings_matrix([:work_package_list_default_columns, :work_package_list_summable_columns],
|
||||
<%= settings_matrix([:work_package_list_default_columns],
|
||||
column_choices, label_choices: :setting_work_package_properties) %>
|
||||
</fieldset>
|
||||
<%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %>
|
||||
|
||||
@@ -2215,7 +2215,6 @@ en:
|
||||
setting_work_package_done_ratio_status: "Use the work package status"
|
||||
setting_work_package_done_ratio_disabled: "Disable (hide the progress)"
|
||||
setting_work_package_list_default_columns: "Display by default"
|
||||
setting_work_package_list_summable_columns: "Summable"
|
||||
setting_work_package_properties: "Work package properties"
|
||||
setting_work_package_startdate_is_adddate: "Use current date as start date for new work packages"
|
||||
setting_work_packages_export_limit: "Work packages export limit"
|
||||
|
||||
@@ -270,10 +270,6 @@ work_package_list_default_highlighting_mode:
|
||||
work_package_list_default_highlighted_attributes:
|
||||
serialized: true
|
||||
default: []
|
||||
work_package_list_summable_columns:
|
||||
serialized: true
|
||||
default:
|
||||
- estimated_hours
|
||||
display_subprojects_work_packages:
|
||||
default: 1
|
||||
work_package_done_ratio:
|
||||
|
||||
@@ -98,14 +98,6 @@ Given(/^the custom field "(.*?)" is disabled for the project "(.*?)"$/) do |fiel
|
||||
project.work_package_custom_fields.delete custom_field
|
||||
end
|
||||
|
||||
Given /^the custom field "(.+)" is( not)? summable$/ do |field_name, negative|
|
||||
custom_field = WorkPackageCustomField.find_by(name: field_name)
|
||||
|
||||
Setting.work_package_list_summable_columns = negative ?
|
||||
Setting.work_package_list_summable_columns - ["cf_#{custom_field.id}"] :
|
||||
Setting.work_package_list_summable_columns << "cf_#{custom_field.id}"
|
||||
end
|
||||
|
||||
Given /^the custom field "(.*?)" is activated for type "(.*?)"$/ do |field_name, type_name|
|
||||
custom_field = WorkPackageCustomField.find_by(name: field_name)
|
||||
type = ::Type.find_by(name: type_name)
|
||||
|
||||
@@ -41,11 +41,7 @@ module API
|
||||
name_source: ->(*) { custom_field.name },
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
Setting.work_package_list_summable_columns.any? do |column_name|
|
||||
/cf_(\d+)/.match(column_name)
|
||||
end
|
||||
}
|
||||
show_if: ->(*) { custom_field.summable? }
|
||||
end
|
||||
|
||||
def inject_property_value(custom_field)
|
||||
@@ -53,10 +49,7 @@ module API
|
||||
getter: property_value_getter_for(custom_field),
|
||||
setter: property_value_setter_for(custom_field),
|
||||
render_nil: true,
|
||||
if: ->(*) {
|
||||
setting = ::Setting.work_package_list_summable_columns
|
||||
setting.include?("cf_#{custom_field.id}")
|
||||
}
|
||||
if: ->(*) { custom_field.summable? }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,10 +54,32 @@ module API
|
||||
schema :estimated_time,
|
||||
type: 'Duration',
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('estimated_hours')
|
||||
}
|
||||
writable: false
|
||||
|
||||
schema :story_points,
|
||||
type: 'Integer',
|
||||
required: false
|
||||
|
||||
schema :remaining_time,
|
||||
type: 'Duration',
|
||||
name_source: :remaining_hours,
|
||||
required: false,
|
||||
writable: false
|
||||
|
||||
schema :overall_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false
|
||||
|
||||
schema :labor_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false
|
||||
|
||||
schema :material_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ module API
|
||||
module WorkPackages
|
||||
class WorkPackageSumsRepresenter < ::API::Decorators::Single
|
||||
extend ::API::V3::Utilities::CustomFieldInjector::RepresenterClass
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
custom_field_injector(injector_class: ::API::V3::Utilities::CustomFieldSumInjector)
|
||||
|
||||
@@ -22,9 +23,35 @@ module API
|
||||
getter: ->(*) {
|
||||
datetime_formatter.format_duration_from_hours(represented.estimated_hours,
|
||||
allow_nil: true)
|
||||
},
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('estimated_hours')
|
||||
}
|
||||
|
||||
property :story_points,
|
||||
render_nil: true
|
||||
|
||||
property :remaining_time,
|
||||
render_nil: true,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
datetime_formatter.format_duration_from_hours(represented.remaining_hours,
|
||||
allow_nil: true)
|
||||
}
|
||||
|
||||
property :overall_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.overall_costs)
|
||||
}
|
||||
|
||||
property :labor_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.labor_costs)
|
||||
}
|
||||
|
||||
property :material_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.material_costs)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
require 'open_project/plugins'
|
||||
require_relative './patches/api/work_package_representer'
|
||||
require_relative './patches/api/work_package_schema_representer'
|
||||
require_relative './patches/api/work_package_sums_representer'
|
||||
require_relative './patches/api/work_package_sums_schema_representer'
|
||||
|
||||
module OpenProject::Backlogs
|
||||
class Engine < ::Rails::Engine
|
||||
@@ -149,12 +147,6 @@ module OpenProject::Backlogs
|
||||
extend_api_response(:v3, :work_packages, :schema, :work_package_schema,
|
||||
&::OpenProject::Backlogs::Patches::API::WorkPackageSchemaRepresenter.extension)
|
||||
|
||||
extend_api_response(:v3, :work_packages, :schema, :work_package_sums_schema,
|
||||
&::OpenProject::Backlogs::Patches::API::WorkPackageSumsSchemaRepresenter.extension)
|
||||
|
||||
extend_api_response(:v3, :work_packages, :work_package_sums,
|
||||
&::OpenProject::Backlogs::Patches::API::WorkPackageSumsRepresenter.extension)
|
||||
|
||||
add_api_attribute on: :work_package, ar_name: :story_points
|
||||
add_api_attribute on: :work_package, ar_name: :remaining_hours, writeable: ->(*) { model.leaf? }
|
||||
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module OpenProject::Backlogs
|
||||
module Patches
|
||||
module API
|
||||
module WorkPackageSumsRepresenter
|
||||
module_function
|
||||
|
||||
def extension
|
||||
->(*) do
|
||||
property :story_points,
|
||||
render_nil: true,
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('story_points')
|
||||
}
|
||||
|
||||
property :remaining_time,
|
||||
render_nil: true,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
datetime_formatter.format_duration_from_hours(represented.remaining_hours,
|
||||
allow_nil: true)
|
||||
},
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('remaining_hours')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module OpenProject::Backlogs
|
||||
module Patches
|
||||
module API
|
||||
module WorkPackageSumsSchemaRepresenter
|
||||
module_function
|
||||
|
||||
def extension
|
||||
->(*) do
|
||||
schema :story_points,
|
||||
type: 'Integer',
|
||||
required: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('story_points')
|
||||
}
|
||||
|
||||
schema :remaining_time,
|
||||
type: 'Duration',
|
||||
name_source: :remaining_hours,
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('remaining_hours')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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'
|
||||
|
||||
describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do
|
||||
let(:current_user) do
|
||||
FactoryBot.build_stubbed(:user)
|
||||
end
|
||||
|
||||
let(:schema) { ::API::V3::WorkPackages::Schema::WorkPackageSumsSchema.new }
|
||||
|
||||
let(:representer) { described_class.create(schema, current_user: current_user) }
|
||||
subject { representer.to_json }
|
||||
|
||||
describe 'storyPoints' do
|
||||
let(:setting) { ['story_points'] }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(setting)
|
||||
end
|
||||
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'storyPoints' }
|
||||
let(:type) { 'Integer' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.story_points') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
|
||||
context 'not marked as summable' do
|
||||
let(:setting) { [] }
|
||||
|
||||
it 'does not show story points' do
|
||||
is_expected.to_not have_json_path('storyPoints')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remainingTime' do
|
||||
let(:setting) { ['remaining_time'] }
|
||||
|
||||
shared_examples_for 'has schema for remainingTime' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'remainingTime' }
|
||||
let(:type) { 'Duration' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.remaining_hours') }
|
||||
let(:required) { false }
|
||||
let(:writable) { true }
|
||||
end
|
||||
end
|
||||
|
||||
context 'not marked as summable' do
|
||||
let(:setting) { [] }
|
||||
|
||||
it 'does not show remaining time' do
|
||||
is_expected.to_not have_json_path('remaining time')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,80 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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'
|
||||
|
||||
describe ::API::V3::WorkPackages::WorkPackageSumsRepresenter do
|
||||
let(:sums) { double 'sums', story_points: 5, remaining_hours: 10 }
|
||||
let(:schema) { double 'schema', available_custom_fields: [] }
|
||||
let(:user) { FactoryBot.build_stubbed(:user) }
|
||||
let(:representer) {
|
||||
described_class.create_class(schema, user).new(sums)
|
||||
}
|
||||
let(:summable_columns) { [] }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(summable_columns)
|
||||
end
|
||||
|
||||
subject { representer.to_json }
|
||||
|
||||
context 'remainingTime' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['remaining_hours'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = 'PT10H'
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('remainingTime')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('remainingTime')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'storyPoints' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['story_points'] }
|
||||
|
||||
it 'is represented' do
|
||||
expect(subject).to be_json_eql(sums.story_points.to_json).at_path('storyPoints')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('storyPoints')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -246,61 +246,6 @@ module Costs
|
||||
writable: false
|
||||
end
|
||||
|
||||
extend_api_response(:v3, :work_packages, :schema, :work_package_sums_schema) do
|
||||
schema :overall_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('overall_costs')
|
||||
}
|
||||
schema :labor_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('labor_costs')
|
||||
}
|
||||
schema :material_costs,
|
||||
type: 'String',
|
||||
required: false,
|
||||
writable: false,
|
||||
show_if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('material_costs')
|
||||
}
|
||||
end
|
||||
|
||||
extend_api_response(:v3, :work_packages, :work_package_sums) do
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
property :overall_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.overall_costs)
|
||||
},
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('overall_costs')
|
||||
}
|
||||
|
||||
property :labor_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.labor_costs)
|
||||
},
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('labor_costs')
|
||||
}
|
||||
|
||||
property :material_costs,
|
||||
exec_context: :decorator,
|
||||
getter: ->(*) {
|
||||
number_to_currency(represented.material_costs)
|
||||
},
|
||||
if: ->(*) {
|
||||
::Setting.work_package_list_summable_columns.include?('material_costs')
|
||||
}
|
||||
end
|
||||
|
||||
initializer 'costs.register_latest_project_activity' do
|
||||
Project.register_latest_project_activity on: 'TimeEntry',
|
||||
attribute: :updated_on
|
||||
|
||||
@@ -33,9 +33,6 @@ module Costs
|
||||
|
||||
def initialize(name, options = {})
|
||||
super
|
||||
|
||||
@sum_function = options[:summable]
|
||||
self.summable = @sum_function.respond_to?(:call)
|
||||
end
|
||||
|
||||
def value(work_package)
|
||||
@@ -54,40 +51,40 @@ module Costs
|
||||
super_value work_package
|
||||
end
|
||||
|
||||
def sum_of(work_packages)
|
||||
@sum_function.call(work_packages)
|
||||
end
|
||||
|
||||
class_attribute :currency_columns
|
||||
|
||||
self.currency_columns = {
|
||||
budget: {},
|
||||
material_costs: {
|
||||
summable: ->(work_packages) {
|
||||
WorkPackage::MaterialCosts
|
||||
.new(user: User.current)
|
||||
.costs_of(work_packages: work_packages)
|
||||
summable: ->(query, grouped) {
|
||||
scope = WorkPackage::MaterialCosts
|
||||
.new(user: User.current)
|
||||
.add_to_work_package_collection(WorkPackage.where(id: query.results.work_packages))
|
||||
.except(:order, :select)
|
||||
|
||||
Queries::WorkPackages::Columns::WorkPackageColumn
|
||||
.scoped_column_sum(scope,
|
||||
"COALESCE(ROUND(SUM(cost_entries_sum), 2)::FLOAT, 0.0) material_costs",
|
||||
grouped && query.group_by_statement)
|
||||
}
|
||||
},
|
||||
labor_costs: {
|
||||
summable: ->(work_packages) {
|
||||
WorkPackage::LaborCosts
|
||||
.new(user: User.current)
|
||||
.costs_of(work_packages: work_packages)
|
||||
summable: ->(query, grouped) {
|
||||
scope = WorkPackage::LaborCosts
|
||||
.new(user: User.current)
|
||||
.add_to_work_package_collection(WorkPackage.where(id: query.results.work_packages))
|
||||
.except(:order, :select)
|
||||
|
||||
Queries::WorkPackages::Columns::WorkPackageColumn
|
||||
.scoped_column_sum(scope,
|
||||
"COALESCE(ROUND(SUM(time_entries_sum), 2)::FLOAT, 0.0) labor_costs",
|
||||
grouped && query.group_by_statement)
|
||||
}
|
||||
},
|
||||
overall_costs: {
|
||||
summable: ->(work_packages) {
|
||||
labor_costs = WorkPackage::LaborCosts
|
||||
.new(user: User.current)
|
||||
.costs_of(work_packages: work_packages)
|
||||
|
||||
material_costs = WorkPackage::MaterialCosts
|
||||
.new(user: User.current)
|
||||
.costs_of(work_packages: work_packages)
|
||||
|
||||
labor_costs + material_costs
|
||||
}
|
||||
summable: true,
|
||||
summable_select: "labor_costs + material_costs AS overall_costs",
|
||||
summable_work_packages_select: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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 File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
||||
|
||||
describe 'Work Package table cost sums', type: :feature, js: true do
|
||||
let(:project) { work_package.project }
|
||||
let(:user) { FactoryBot.create :user,
|
||||
member_in_project: project,
|
||||
member_through_role: role }
|
||||
let(:role) { FactoryBot.create :role, permissions: [:view_own_hourly_rate,
|
||||
:view_work_packages,
|
||||
:view_work_packages,
|
||||
:view_own_time_entries,
|
||||
:view_own_cost_entries,
|
||||
:view_cost_rates,
|
||||
:log_costs] }
|
||||
let(:work_package) {FactoryBot.create :work_package }
|
||||
let(:hourly_rate) { FactoryBot.create :default_hourly_rate, user: user,
|
||||
rate: 1.00 }
|
||||
let!(:time_entry) { FactoryBot.create :time_entry, user: user,
|
||||
work_package: work_package,
|
||||
project: project,
|
||||
hours: 1.50 }
|
||||
let(:cost_type) {
|
||||
type = FactoryBot.create :cost_type, name: 'Translations'
|
||||
FactoryBot.create :cost_rate, cost_type: type,
|
||||
rate: 1.00
|
||||
type
|
||||
}
|
||||
let!(:cost_entry) { FactoryBot.create :cost_entry, work_package: work_package,
|
||||
project: project,
|
||||
units: 2.50,
|
||||
cost_type: cost_type,
|
||||
user: user }
|
||||
let(:wp_table) { ::Pages::WorkPackagesTable.new(project) }
|
||||
let!(:query) do
|
||||
query = FactoryBot.build(:query, user: user, project: project)
|
||||
query.column_names = %w(subject overall_costs material_costs overall_costs)
|
||||
|
||||
query.save!
|
||||
query
|
||||
end
|
||||
|
||||
before do
|
||||
login_as(user)
|
||||
allow(Setting).to receive(:work_package_list_summable_columns).and_return(summable)
|
||||
|
||||
wp_table.visit_query(query)
|
||||
wp_table.expect_work_package_listed(work_package)
|
||||
|
||||
# Trigger action from action menu dropdown
|
||||
wp_table.click_setting_item 'Display sums'
|
||||
expect(page).to have_selector('tr.sum.group.all')
|
||||
end
|
||||
|
||||
context 'when summing enabled' do
|
||||
let(:summable) { %w(overall_costs labor_costs material_costs) }
|
||||
|
||||
it 'shows the sums' do
|
||||
within('tr.sum.group.all') do
|
||||
expect(page).to have_selector('.inline-edit--display-field', text: '2.50 EUR', count: 3)
|
||||
expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
context 'when summing disabled' do
|
||||
let(:summable) { [] }
|
||||
|
||||
it 'does not show the sums' do
|
||||
within('tr.sum.group.all') do
|
||||
expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,98 +0,0 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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'
|
||||
|
||||
describe ::API::V3::WorkPackages::WorkPackageSumsRepresenter do
|
||||
let(:sums) { double 'sums', material_costs: 5, labor_costs: 10, overall_costs: 15 }
|
||||
let(:schema) { double 'schema', available_custom_fields: [] }
|
||||
let(:user) { FactoryBot.build_stubbed(:user) }
|
||||
let(:representer) do
|
||||
described_class.create_class(schema, user).new(sums)
|
||||
end
|
||||
let(:summable_columns) { [] }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(summable_columns)
|
||||
end
|
||||
|
||||
subject { representer.to_json }
|
||||
|
||||
context 'materialCosts' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['material_costs'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = "5.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('materialCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('materialCosts')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'laborCosts' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['labor_costs'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = "10.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('laborCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('laborCosts')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'overallCosts' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['overall_costs'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = "15.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('overallCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('overallCosts')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,145 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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'
|
||||
|
||||
describe Costs::QueryCurrencyColumn, type: :model do
|
||||
let(:project) do
|
||||
FactoryBot.build_stubbed(:project).tap do |p|
|
||||
allow(p)
|
||||
.to receive(:costs_enabled?)
|
||||
.and_return(costs_enabled)
|
||||
end
|
||||
end
|
||||
let(:instance) { described_class.instances(project).detect { |c| c.name == column_name } }
|
||||
let(:costs_enabled) { true }
|
||||
let(:column_name) { :material_costs }
|
||||
|
||||
describe '.instances' do
|
||||
subject { described_class.instances(project).map(&:name) }
|
||||
|
||||
context 'with costs enabled' do
|
||||
it 'returns the four costs columns' do
|
||||
is_expected
|
||||
.to match_array %i[budget material_costs labor_costs overall_costs]
|
||||
end
|
||||
end
|
||||
|
||||
context 'with costs disabled' do
|
||||
let(:costs_enabled) { false }
|
||||
|
||||
it 'returns no columns' do
|
||||
is_expected
|
||||
.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no context' do
|
||||
it 'returns the four costs columns' do
|
||||
is_expected
|
||||
.to match_array %i[budget material_costs labor_costs overall_costs]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'material_costs' do
|
||||
describe '#summable?' do
|
||||
it 'is true' do
|
||||
expect(instance)
|
||||
.to be_summable
|
||||
end
|
||||
end
|
||||
|
||||
describe '#summable' do
|
||||
it 'is callable' do
|
||||
expect(instance.summable)
|
||||
.to respond_to(:call)
|
||||
end
|
||||
|
||||
# Not testing the results here, this is done by an integration test
|
||||
it 'returns an AR scope that has an id and a material_costs column' do
|
||||
query = double('query')
|
||||
result = double('result')
|
||||
|
||||
allow(query)
|
||||
.to receive(:results)
|
||||
.and_return result
|
||||
|
||||
allow(result)
|
||||
.to receive(:work_packages)
|
||||
.and_return(WorkPackage.all)
|
||||
|
||||
allow(query)
|
||||
.to receive(:group_by_statement)
|
||||
.and_return('author_id')
|
||||
|
||||
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).column_types.keys)
|
||||
.to match_array %w(id material_costs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'labor_costs' do
|
||||
let(:column_name) { :labor_costs }
|
||||
|
||||
describe '#summable?' do
|
||||
it 'is true' do
|
||||
expect(instance)
|
||||
.to be_summable
|
||||
end
|
||||
end
|
||||
|
||||
describe '#summable' do
|
||||
it 'is callable' do
|
||||
expect(instance.summable)
|
||||
.to respond_to(:call)
|
||||
end
|
||||
|
||||
# Not testing the results here, this is done by an integration test
|
||||
it 'returns an AR scope that has an id and a labor_costs column' do
|
||||
query = double('query')
|
||||
result = double('result')
|
||||
|
||||
allow(query)
|
||||
.to receive(:results)
|
||||
.and_return result
|
||||
|
||||
allow(result)
|
||||
.to receive(:work_packages)
|
||||
.and_return(WorkPackage.all)
|
||||
|
||||
allow(query)
|
||||
.to receive(:group_by_statement)
|
||||
.and_return('author_id')
|
||||
|
||||
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).column_types.keys)
|
||||
.to match_array %w(id labor_costs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,8 +29,17 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.feature 'Work package index sums', js: true do
|
||||
using_shared_fixtures :admin
|
||||
|
||||
let(:user) do
|
||||
FactoryBot.create :user,
|
||||
member_in_project: project,
|
||||
member_with_permissions: %i[view_own_hourly_rate
|
||||
view_work_packages
|
||||
edit_work_packages
|
||||
view_time_entries
|
||||
view_cost_entries
|
||||
view_cost_rates
|
||||
log_costs]
|
||||
end
|
||||
let(:project) do
|
||||
FactoryBot.create(:project, name: 'project1', identifier: 'project1')
|
||||
end
|
||||
@@ -59,6 +68,33 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
wp.save!
|
||||
end
|
||||
end
|
||||
let!(:hourly_rate) do
|
||||
FactoryBot.create :default_hourly_rate,
|
||||
user: user,
|
||||
rate: 10.00
|
||||
end
|
||||
let!(:time_entry) do
|
||||
FactoryBot.create :time_entry,
|
||||
user: user,
|
||||
work_package: work_package_1,
|
||||
project: project,
|
||||
hours: 1.50
|
||||
end
|
||||
let(:cost_type) do
|
||||
type = FactoryBot.create :cost_type, name: 'Translations'
|
||||
FactoryBot.create :cost_rate,
|
||||
cost_type: type,
|
||||
rate: 3.00
|
||||
type
|
||||
end
|
||||
let!(:cost_entry) do
|
||||
FactoryBot.create :cost_entry,
|
||||
work_package: work_package_1,
|
||||
project: project,
|
||||
units: 2.50,
|
||||
cost_type: cost_type,
|
||||
user: user
|
||||
end
|
||||
|
||||
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
|
||||
let(:columns) { ::Components::WorkPackages::Columns.new }
|
||||
@@ -66,17 +102,13 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
let(:group_by) { ::Components::WorkPackages::GroupBy.new }
|
||||
|
||||
before do
|
||||
login_as(admin)
|
||||
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(%W(estimated_hours cf_#{int_cf.id} cf_#{float_cf.id}))
|
||||
login_as(user)
|
||||
|
||||
visit project_work_packages_path(project)
|
||||
expect(current_path).to eq('/projects/project1/work_packages')
|
||||
end
|
||||
|
||||
scenario 'calculates summs correctly' do
|
||||
scenario 'calculates sums correctly' do
|
||||
wp_table.expect_work_package_listed work_package_1, work_package_2
|
||||
|
||||
# Add estimated time column
|
||||
@@ -85,6 +117,12 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
columns.add int_cf.name
|
||||
# Add float cf column
|
||||
columns.add float_cf.name
|
||||
# Add overall costs column
|
||||
columns.add 'Overall costs'
|
||||
# Add unit costs column
|
||||
columns.add 'Unit costs'
|
||||
# Add labor costs column
|
||||
columns.add 'Labor costs'
|
||||
|
||||
# Trigger action from action menu dropdown
|
||||
modal.set_display_sums enable: true
|
||||
@@ -98,6 +136,12 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '25')
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '12')
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '13.2')
|
||||
# Unit costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '7.50')
|
||||
# Overall costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '22.50')
|
||||
# Labor costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '15.00')
|
||||
|
||||
# Update the sum
|
||||
edit_field = wp_table.edit_field(work_package_1, :estimatedTime)
|
||||
@@ -107,17 +151,29 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '35')
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '12')
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '13.2')
|
||||
# Unit costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '7.50')
|
||||
# Overall costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '22.50')
|
||||
# Labor costs
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '15.00')
|
||||
|
||||
# Enable groups
|
||||
group_by.enable_via_menu 'Status'
|
||||
|
||||
# Expect to have three sums rows no
|
||||
# Expect to have three sums rows now
|
||||
expect(page).to have_selector('.wp-table--sums-row', count: 3)
|
||||
|
||||
# First status row
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '20 h')
|
||||
expect(page).to have_selector(".wp-table--sum-container.customField#{int_cf.id}", text: '5')
|
||||
expect(page).to have_selector(".wp-table--sum-container.customField#{float_cf.id}", text: '5.5')
|
||||
# Unit costs
|
||||
expect(page).to have_selector('.wp-table--sum-container.materialCosts', text: '7.50')
|
||||
# Overall costs
|
||||
expect(page).to have_selector('.wp-table--sum-container.overallCosts', text: '22.50')
|
||||
# Labor costs
|
||||
expect(page).to have_selector('.wp-table--sum-container.laborCosts', text: '15.00')
|
||||
|
||||
# Second status row
|
||||
expect(page).to have_selector('.wp-table--sum-container', text: '15 h')
|
||||
@@ -128,6 +184,12 @@ RSpec.feature 'Work package index sums', js: true do
|
||||
expect(page).to have_selector('tfoot .wp-table--sum-container', text: '35')
|
||||
expect(page).to have_selector("tfoot .wp-table--sum-container.customField#{int_cf.id}", text: '12')
|
||||
expect(page).to have_selector("tfoot .wp-table--sum-container.customField#{float_cf.id}", text: '13.2')
|
||||
# Unit costs
|
||||
expect(page).to have_selector('tfoot .wp-table--sum-container.materialCosts', text: '7.50')
|
||||
# Overall costs
|
||||
expect(page).to have_selector('tfoot .wp-table--sum-container.overallCosts', text: '22.50')
|
||||
# Labor costs
|
||||
expect(page).to have_selector('tfoot .wp-table--sum-container.laborCosts', text: '15.00')
|
||||
|
||||
# Collapsing groups will also hide the sums row
|
||||
page.find('.expander.icon-minus2', match: :first).click
|
||||
|
||||
@@ -31,20 +31,14 @@ require 'spec_helper'
|
||||
describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do
|
||||
include ::API::V3::Utilities::PathHelper
|
||||
|
||||
let(:available_custom_fields) { [] }
|
||||
let(:custom_field) { FactoryBot.build_stubbed(:integer_issue_custom_field) }
|
||||
let(:available_custom_fields) { [custom_field] }
|
||||
let(:schema) { double('wp_schema', available_custom_fields: available_custom_fields) }
|
||||
let(:current_user) { double('user', admin?: false) }
|
||||
|
||||
let(:representer) do
|
||||
described_class.create(schema, current_user: current_user)
|
||||
end
|
||||
let(:summable_columns) { [] }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(summable_columns)
|
||||
end
|
||||
|
||||
subject { representer.to_json }
|
||||
|
||||
@@ -59,49 +53,72 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do
|
||||
end
|
||||
|
||||
context 'estimated_time' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['estimated_hours'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = { 'type': 'Duration',
|
||||
'name': 'Estimated time',
|
||||
'required': false,
|
||||
'hasDefault': false,
|
||||
'writable': false,
|
||||
'options': {} }
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('estimatedTime')
|
||||
end
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'estimatedTime' }
|
||||
let(:type) { 'Duration' }
|
||||
let(:name) { I18n.t('attributes.estimated_hours') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('estimatedTime')
|
||||
end
|
||||
describe 'storyPoints' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'storyPoints' }
|
||||
let(:type) { 'Integer' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.story_points') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remainingTime' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'remainingTime' }
|
||||
let(:type) { 'Duration' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.remaining_hours') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'overallCosts' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'overallCosts' }
|
||||
let(:type) { 'String' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.overall_costs') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'laborCosts' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'laborCosts' }
|
||||
let(:type) { 'String' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.labor_costs') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'materialCosts' do
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { 'materialCosts' }
|
||||
let(:type) { 'String' }
|
||||
let(:name) { I18n.t('activerecord.attributes.work_package.material_costs') }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'custom field x' do
|
||||
let(:custom_field) { FactoryBot.build_stubbed(:integer_issue_custom_field) }
|
||||
let(:available_custom_fields) { [custom_field] }
|
||||
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ["cf_#{custom_field.id}"] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = { 'type': 'Integer',
|
||||
'name': custom_field.name,
|
||||
'required': false,
|
||||
'hasDefault': false,
|
||||
'writable': false,
|
||||
'options': {} }
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path("customField#{custom_field.id}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path("customField#{custom_field.id}")
|
||||
end
|
||||
it_behaves_like 'has basic schema properties' do
|
||||
let(:path) { "customField#{custom_field.id}" }
|
||||
let(:type) { 'Integer' }
|
||||
let(:name) { custom_field.name }
|
||||
let(:required) { false }
|
||||
let(:writable) { false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -387,7 +387,9 @@ describe ::API::V3::WorkPackages::WorkPackageCollectionRepresenter do
|
||||
let(:total_sums) { OpenStruct.new(estimated_hours: 1) }
|
||||
|
||||
it 'renders the groups object as json' do
|
||||
expected = { 'estimatedTime': 'PT1H' }
|
||||
expected = { 'estimatedTime': 'PT1H',
|
||||
'remainingTime': nil,
|
||||
'storyPoints': nil }
|
||||
is_expected.to be_json_eql(expected.to_json).at_path('totalSums')
|
||||
end
|
||||
|
||||
|
||||
@@ -29,57 +29,70 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe ::API::V3::WorkPackages::WorkPackageSumsRepresenter do
|
||||
let(:available_custom_fields) { [] }
|
||||
let(:sums) { double 'sums', estimated_hours: 5 }
|
||||
let(:schema) { double 'schema', available_custom_fields: available_custom_fields }
|
||||
let(:custom_field) { FactoryBot.build_stubbed(:int_wp_custom_field, id: 1) }
|
||||
let(:sums) do
|
||||
double('sums',
|
||||
story_points: 5,
|
||||
remaining_hours: 10,
|
||||
estimated_hours: 5,
|
||||
material_costs: 5,
|
||||
labor_costs: 10,
|
||||
overall_costs: 15,
|
||||
custom_field_1: 5,
|
||||
available_custom_fields: [custom_field])
|
||||
end
|
||||
let(:schema) { double 'schema', available_custom_fields: [custom_field] }
|
||||
let(:current_user) { FactoryBot.build_stubbed(:user) }
|
||||
let(:representer) do
|
||||
described_class.create_class(schema, current_user).new(sums)
|
||||
end
|
||||
let(:summable_columns) { [] }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(summable_columns)
|
||||
end
|
||||
|
||||
subject { representer.to_json }
|
||||
|
||||
context 'estimated_time' do
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ['estimated_hours'] }
|
||||
|
||||
it 'is represented' do
|
||||
expected = 'PT5H'
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('estimatedTime')
|
||||
end
|
||||
it 'is represented' do
|
||||
expected = 'PT5H'
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('estimatedTime')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path('estimatedTime')
|
||||
end
|
||||
context 'remainingTime' do
|
||||
it 'is represented' do
|
||||
expected = 'PT10H'
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('remainingTime')
|
||||
end
|
||||
end
|
||||
|
||||
context 'storyPoints' do
|
||||
it 'is represented' do
|
||||
expect(subject).to be_json_eql(sums.story_points.to_json).at_path('storyPoints')
|
||||
end
|
||||
end
|
||||
|
||||
context 'materialCosts' do
|
||||
it 'is represented' do
|
||||
expected = "5.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('materialCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'laborCosts' do
|
||||
it 'is represented' do
|
||||
expected = "10.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('laborCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'overallCosts' do
|
||||
it 'is represented' do
|
||||
expected = "15.00 EUR"
|
||||
expect(subject).to be_json_eql(expected.to_json).at_path('overallCosts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'custom field x' do
|
||||
let(:custom_field) { FactoryBot.build_stubbed(:int_wp_custom_field, id: 1) }
|
||||
let(:available_custom_fields) { [custom_field] }
|
||||
let(:sums) { double 'sums', available_custom_fields: available_custom_fields, custom_field_1: 5 }
|
||||
|
||||
context 'with it being configured to be summable' do
|
||||
let(:summable_columns) { ["cf_#{custom_field.id}"] }
|
||||
|
||||
it 'is represented' do
|
||||
expect(subject).to be_json_eql(sums.custom_field_1.to_json).at_path('customField1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without it being configured to be summable' do
|
||||
it 'is not represented when the summable setting does not list it' do
|
||||
expect(subject).to_not have_json_path("customField#{custom_field.id}")
|
||||
end
|
||||
it 'is represented' do
|
||||
expect(subject).to be_json_eql(sums.custom_field_1.to_json).at_path('customField1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,7 @@ describe OpenProject::OmniAuth::Authorization do
|
||||
let(:user) { FactoryBot.create :user, mail: 'foo@bar.de' }
|
||||
let(:state) { Struct.new(:number, :user_email, :uid).new 0, nil, nil }
|
||||
let(:collector) { [] }
|
||||
let!(:existing_callbacks) { OpenProject::OmniAuth::Authorization.after_login_callbacks.dup }
|
||||
|
||||
before do
|
||||
OpenProject::OmniAuth::Authorization.after_login_callbacks.clear
|
||||
@@ -53,7 +54,13 @@ describe OpenProject::OmniAuth::Authorization do
|
||||
end
|
||||
|
||||
after do
|
||||
# Reset existing callbacks to avoid sideeffects
|
||||
OpenProject::OmniAuth::Authorization.after_login_callbacks.clear
|
||||
callbacks = OpenProject::OmniAuth::Authorization.after_login_callbacks
|
||||
|
||||
existing_callbacks.each do |callback_block|
|
||||
callbacks << callback_block
|
||||
end
|
||||
end
|
||||
|
||||
it 'triggers every callback setting uid to "bar", number to 42 and user_email to foo@bar.de' do
|
||||
|
||||
@@ -36,59 +36,4 @@ describe Queries::WorkPackages::Columns::WorkPackageColumn, type: :model do
|
||||
it "allows to be constructed without attribute highlightable" do
|
||||
expect(described_class.new('foo').highlightable?).to eq(false)
|
||||
end
|
||||
|
||||
describe "sum of" do
|
||||
describe :estimated_hours do
|
||||
context "with work packages in a hierarchy" do
|
||||
let(:work_packages) do
|
||||
hierarchy = [
|
||||
["Single", 1, 0],
|
||||
{
|
||||
["Parent", 1, 3] => [
|
||||
["Child 1 of Parent", 1, 0],
|
||||
["Child 2 of Parent", 1, 0],
|
||||
["Hidden Child 3 of Parent", 1, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
["Hidden Parent", 5, 4] => [
|
||||
["Child of Hidden Parent", 1, 0],
|
||||
["Hidden Child", 3, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
["Parent 2", 1, 3] => [
|
||||
["Child 1 of Parent 2", 1, 0],
|
||||
{
|
||||
["Nested Parent", 0, 2] => [
|
||||
["Child 1 of Nested Parent", 1, 0],
|
||||
["Child 2 of Nested Parent", 1, 0]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
build_work_package_hierarchy hierarchy, :subject, :estimated_hours, :derived_estimated_hours
|
||||
end
|
||||
|
||||
let(:result_set) { WorkPackage.where("NOT subject LIKE 'Hidden%'") }
|
||||
let(:column) { Queries::WorkPackages::Columns::WorkPackageColumn.new :estimated_hours }
|
||||
|
||||
before do
|
||||
work_packages # create work packages
|
||||
|
||||
expect(WorkPackage.count).to eq work_packages.size
|
||||
expect(result_set.count).to eq(work_packages.size - 3) # all work packages except the hidden parent and children
|
||||
end
|
||||
|
||||
it "yields the correct sum, not counting any children (of parents in the result set) twice" do
|
||||
# Single + Parent + Child 1 of Parent + Child 2 of Parent + Child of Hidden Parent + Parent 2 + Child 1 of Parent 2
|
||||
# + Nested Parent + Child 1 of Nested Parent + Child 2 of Nested Parent
|
||||
expect(column.sum_of(result_set)).to eq 9
|
||||
expect(column.sum_of(WorkPackage.all)).to eq 18 # the above + Hidden Child 3 of Parent + Hidden Parent + Hidden Child
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
#
|
||||
# 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'
|
||||
|
||||
describe ::Query::Results, 'sums', type: :model do
|
||||
let(:project) do
|
||||
FactoryBot.create(:project).tap do |p|
|
||||
p.work_package_custom_fields << int_cf
|
||||
p.work_package_custom_fields << float_cf
|
||||
end
|
||||
end
|
||||
let(:other_project) do
|
||||
FactoryBot.create(:project).tap do |p|
|
||||
p.work_package_custom_fields << int_cf
|
||||
p.work_package_custom_fields << float_cf
|
||||
end
|
||||
end
|
||||
let!(:work_package1) do
|
||||
FactoryBot.create(:work_package,
|
||||
type: type,
|
||||
project: project,
|
||||
estimated_hours: 5,
|
||||
done_ratio: 10,
|
||||
"custom_field_#{int_cf.id}" => 10,
|
||||
"custom_field_#{float_cf.id}" => 3.414,
|
||||
remaining_hours: 3,
|
||||
story_points: 7)
|
||||
end
|
||||
let!(:work_package2) do
|
||||
FactoryBot.create(:work_package,
|
||||
type: type,
|
||||
project: project,
|
||||
assigned_to: current_user,
|
||||
done_ratio: 50,
|
||||
estimated_hours: 5,
|
||||
"custom_field_#{int_cf.id}" => 10,
|
||||
"custom_field_#{float_cf.id}" => 3.414,
|
||||
remaining_hours: 3,
|
||||
story_points: 7)
|
||||
end
|
||||
let!(:work_package3) do
|
||||
FactoryBot.create(:work_package,
|
||||
type: type,
|
||||
project: project,
|
||||
assigned_to: current_user,
|
||||
responsible: current_user,
|
||||
done_ratio: 50,
|
||||
estimated_hours: 5,
|
||||
"custom_field_#{int_cf.id}" => 10,
|
||||
"custom_field_#{float_cf.id}" => 3.414,
|
||||
remaining_hours: 3,
|
||||
story_points: 7)
|
||||
end
|
||||
let!(:invisible_work_package1) do
|
||||
FactoryBot.create(:work_package,
|
||||
type: type,
|
||||
project: other_project,
|
||||
estimated_hours: 5,
|
||||
"custom_field_#{int_cf.id}" => 10,
|
||||
"custom_field_#{float_cf.id}" => 3.414,
|
||||
remaining_hours: 3,
|
||||
story_points: 7)
|
||||
end
|
||||
let!(:cost_entry1) do
|
||||
FactoryBot.create(:cost_entry,
|
||||
project: project,
|
||||
work_package: work_package1,
|
||||
user: current_user,
|
||||
overridden_costs: 200)
|
||||
end
|
||||
let!(:cost_entry2) do
|
||||
FactoryBot.create(:cost_entry,
|
||||
project: project,
|
||||
work_package: work_package2,
|
||||
user: current_user,
|
||||
overridden_costs: 200)
|
||||
end
|
||||
let!(:time_entry1) do
|
||||
FactoryBot.create(:time_entry,
|
||||
project: project,
|
||||
work_package: work_package1,
|
||||
user: current_user,
|
||||
overridden_costs: 300)
|
||||
end
|
||||
let!(:time_entry2) do
|
||||
FactoryBot.create(:time_entry,
|
||||
project: project,
|
||||
work_package: work_package2,
|
||||
user: current_user,
|
||||
overridden_costs: 300)
|
||||
end
|
||||
let(:int_cf) do
|
||||
FactoryBot.create(:int_wp_custom_field)
|
||||
end
|
||||
let(:float_cf) do
|
||||
FactoryBot.create(:float_wp_custom_field)
|
||||
end
|
||||
let(:type) do
|
||||
FactoryBot.create(:type).tap do |t|
|
||||
t.custom_fields << int_cf
|
||||
t.custom_fields << float_cf
|
||||
end
|
||||
end
|
||||
let(:current_user) do
|
||||
FactoryBot.create(:user,
|
||||
member_in_project: project,
|
||||
member_with_permissions: permissions)
|
||||
end
|
||||
let(:permissions) do
|
||||
%i[view_work_packages view_cost_entries view_time_entries view_cost_rates view_hourly_rates]
|
||||
end
|
||||
let(:group_by) { nil }
|
||||
let(:query) do
|
||||
FactoryBot.build :query,
|
||||
project: project,
|
||||
group_by: group_by
|
||||
end
|
||||
let(:query_results) do
|
||||
::Query::Results.new query
|
||||
end
|
||||
|
||||
before do
|
||||
login_as(current_user)
|
||||
end
|
||||
let(:estimated_hours_column) { query.available_columns.detect { |c| c.name.to_s == 'estimated_hours' } }
|
||||
let(:int_cf_column) { query.available_columns.detect { |c| c.name.to_s == "cf_#{int_cf.id}" } }
|
||||
let(:float_cf_column) { query.available_columns.detect { |c| c.name.to_s == "cf_#{float_cf.id}" } }
|
||||
let(:material_costs_column) { query.available_columns.detect { |c| c.name.to_s == "material_costs" } }
|
||||
let(:labor_costs_column) { query.available_columns.detect { |c| c.name.to_s == "labor_costs" } }
|
||||
let(:overall_costs_column) { query.available_columns.detect { |c| c.name.to_s == "overall_costs" } }
|
||||
let(:remaining_hours_column) { query.available_columns.detect { |c| c.name.to_s == "remaining_hours" } }
|
||||
let(:story_points_column) { query.available_columns.detect { |c| c.name.to_s == "story_points" } }
|
||||
|
||||
describe '#all_total_sums' do
|
||||
it 'is a hash of all summable columns' do
|
||||
expect(query_results.all_total_sums)
|
||||
.to eql(estimated_hours_column => 15.0,
|
||||
int_cf_column => 30,
|
||||
float_cf_column => 10.24,
|
||||
material_costs_column => 400.0,
|
||||
labor_costs_column => 600.0,
|
||||
overall_costs_column => 1000.0,
|
||||
remaining_hours_column => 9.0,
|
||||
story_points_column => 21)
|
||||
end
|
||||
|
||||
context 'when filtering' do
|
||||
before do
|
||||
query.add_filter('assigned_to_id', '=', [current_user.id.to_s])
|
||||
end
|
||||
|
||||
it 'is a hash of all summable columns and includes only the work packages matching the filter' do
|
||||
expect(query_results.all_total_sums)
|
||||
.to eql(estimated_hours_column => 10.0,
|
||||
int_cf_column => 20,
|
||||
float_cf_column => 6.83,
|
||||
material_costs_column => 200.0,
|
||||
labor_costs_column => 300.0,
|
||||
overall_costs_column => 500.0,
|
||||
remaining_hours_column => 6.0,
|
||||
story_points_column => 14)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#all_sums_for_group' do
|
||||
context 'grouped by assigned_to' do
|
||||
let(:group_by) { :assigned_to }
|
||||
|
||||
it 'is a hash of sums grouped by user values (and nil) and grouped columns' do
|
||||
expect(query_results.all_group_sums)
|
||||
.to eql(current_user => { estimated_hours_column => 10.0,
|
||||
int_cf_column => 20,
|
||||
float_cf_column => 6.83,
|
||||
material_costs_column => 200.0,
|
||||
labor_costs_column => 300.0,
|
||||
overall_costs_column => 500.0,
|
||||
remaining_hours_column => 6.0,
|
||||
story_points_column => 14 },
|
||||
nil => { estimated_hours_column => 5.0,
|
||||
int_cf_column => 10,
|
||||
float_cf_column => 3.41,
|
||||
material_costs_column => 200.0,
|
||||
labor_costs_column => 300.0,
|
||||
overall_costs_column => 500.0,
|
||||
remaining_hours_column => 3.0,
|
||||
story_points_column => 7 })
|
||||
end
|
||||
|
||||
context 'when filtering' do
|
||||
before do
|
||||
query.add_filter('responsible_id', '=', [current_user.id.to_s])
|
||||
end
|
||||
|
||||
it 'is a hash of sums grouped by user values and grouped columns' do
|
||||
expect(query_results.all_group_sums)
|
||||
.to eql(current_user => { estimated_hours_column => 5.0,
|
||||
int_cf_column => 10,
|
||||
float_cf_column => 3.41,
|
||||
material_costs_column => 0.0,
|
||||
labor_costs_column => 0.0,
|
||||
overall_costs_column => 0.0,
|
||||
story_points_column => 7,
|
||||
remaining_hours_column => 3.0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'grouped by done_ratio' do
|
||||
let(:group_by) { :done_ratio }
|
||||
|
||||
it 'is a hash of sums grouped by done_ratio values and grouped columns' do
|
||||
expect(query_results.all_group_sums)
|
||||
.to eql(50 => { estimated_hours_column => 10.0,
|
||||
int_cf_column => 20,
|
||||
float_cf_column => 6.83,
|
||||
material_costs_column => 200.0,
|
||||
labor_costs_column => 300.0,
|
||||
overall_costs_column => 500.0,
|
||||
remaining_hours_column => 6.0,
|
||||
story_points_column => 14 },
|
||||
10 => { estimated_hours_column => 5.0,
|
||||
int_cf_column => 10,
|
||||
float_cf_column => 3.41,
|
||||
material_costs_column => 200.0,
|
||||
labor_costs_column => 300.0,
|
||||
overall_costs_column => 500.0,
|
||||
remaining_hours_column => 3.0,
|
||||
story_points_column => 7 })
|
||||
end
|
||||
|
||||
context 'when filtering' do
|
||||
before do
|
||||
query.add_filter('responsible_id', '=', [current_user.id.to_s])
|
||||
end
|
||||
|
||||
it 'is a hash of sums grouped by done_ratio values and grouped columns' do
|
||||
expect(query_results.all_group_sums)
|
||||
.to eql(50 => { estimated_hours_column => 5.0,
|
||||
int_cf_column => 10,
|
||||
float_cf_column => 3.41,
|
||||
material_costs_column => 0.0,
|
||||
labor_costs_column => 0.0,
|
||||
overall_costs_column => 0.0,
|
||||
story_points_column => 7,
|
||||
remaining_hours_column => 3.0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,41 +30,20 @@ require 'spec_helper'
|
||||
|
||||
describe WorkPackageCustomField, type: :model do
|
||||
describe '.summable' do
|
||||
let (:custom_field) do
|
||||
FactoryBot.create(:work_package_custom_field,
|
||||
name: 'Database',
|
||||
field_format: 'list',
|
||||
possible_values: %w(MySQL PostgreSQL Oracle),
|
||||
is_required: true)
|
||||
let!(:list_custom_field) do
|
||||
FactoryBot.create(:list_wp_custom_field)
|
||||
end
|
||||
|
||||
before do
|
||||
custom_field.save!
|
||||
let!(:int_custom_field) do
|
||||
FactoryBot.create(:int_wp_custom_field)
|
||||
end
|
||||
let!(:float_custom_field) do
|
||||
FactoryBot.create(:float_wp_custom_field)
|
||||
end
|
||||
|
||||
context 'with a summable field' do
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(["cf_#{custom_field.id}"])
|
||||
end
|
||||
|
||||
it 'contains the custom_field' do
|
||||
expect(described_class.summable)
|
||||
.to match_array [custom_field]
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a summable field' do
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(['blubs'])
|
||||
end
|
||||
|
||||
it 'does not contain the custom_field' do
|
||||
expect(described_class.summable)
|
||||
.to be_empty
|
||||
.to match_array [int_custom_field, float_custom_field]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -219,7 +219,12 @@ describe API::V3::WorkPackages::WorkPackagesByProjectAPI, type: :request do
|
||||
value: priority1.name,
|
||||
count: 2,
|
||||
sums: {
|
||||
estimatedTime: 'PT4H'
|
||||
estimatedTime: 'PT4H',
|
||||
laborCosts: "0.00 EUR",
|
||||
materialCosts: "0.00 EUR",
|
||||
overallCosts: "0.00 EUR",
|
||||
remainingTime: nil,
|
||||
storyPoints: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -237,7 +242,12 @@ describe API::V3::WorkPackages::WorkPackagesByProjectAPI, type: :request do
|
||||
value: priority2.name,
|
||||
count: 1,
|
||||
sums: {
|
||||
estimatedTime: 'PT2H'
|
||||
estimatedTime: 'PT2H',
|
||||
laborCosts: "0.00 EUR",
|
||||
materialCosts: "0.00 EUR",
|
||||
overallCosts: "0.00 EUR",
|
||||
remainingTime: nil,
|
||||
storyPoints: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -265,7 +275,12 @@ describe API::V3::WorkPackages::WorkPackagesByProjectAPI, type: :request do
|
||||
|
||||
it 'contains the sum element' do
|
||||
expected = {
|
||||
estimatedTime: 'PT3H'
|
||||
estimatedTime: 'PT3H',
|
||||
laborCosts: "0.00 EUR",
|
||||
materialCosts: "0.00 EUR",
|
||||
overallCosts: "0.00 EUR",
|
||||
remainingTime: nil,
|
||||
storyPoints: nil
|
||||
}
|
||||
|
||||
expect(subject.body).to be_json_eql(expected.to_json).at_path('totalSums')
|
||||
|
||||
@@ -185,12 +185,6 @@ describe API::V3::WorkPackages::Schema::WorkPackageSchemasAPI, type: :request do
|
||||
let(:schema_path) { api_v3_paths.work_package_sums_schema }
|
||||
subject { last_response }
|
||||
|
||||
before do
|
||||
allow(Setting)
|
||||
.to receive(:work_package_list_summable_columns)
|
||||
.and_return(['estimated_hours'])
|
||||
end
|
||||
|
||||
context 'logged in' do
|
||||
before do
|
||||
allow(User).to receive(:current).and_return(current_user)
|
||||
|
||||
@@ -57,14 +57,9 @@ describe ::API::V3::WorkPackageCollectionFromQueryService,
|
||||
.and_return(1 => 5, 2 => 10)
|
||||
|
||||
allow(results)
|
||||
.to receive(:all_sums_for_group)
|
||||
.with(1)
|
||||
.and_return(OpenStruct.new(name: :status_id) => 50)
|
||||
|
||||
allow(results)
|
||||
.to receive(:all_sums_for_group)
|
||||
.with(2)
|
||||
.and_return(OpenStruct.new(name: :status_id) => 100)
|
||||
.to receive(:all_group_sums)
|
||||
.and_return(1 => { OpenStruct.new(name: :status_id) => 50 },
|
||||
2 => { OpenStruct.new(name: :status_id) => 100 })
|
||||
|
||||
allow(results)
|
||||
.to receive(:query)
|
||||
|
||||
Reference in New Issue
Block a user